CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_utils.py
00001 # -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
00002 #
00003 # This program is free software; you can redistribute it and/or modify
00004 # it under the terms of the GNU General Public License as published by
00005 # the Free Software Foundation; either version 2, or (at your option)
00006 # any later version.
00007 #
00008 # This program is distributed in the hope that it will be useful,
00009 # but WITHOUT ANY WARRANTY; without even the implied warranty of
00010 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
00011 # GNU General Public License for more details.
00012 #
00013 # You should have received a copy of the GNU General Public License
00014 # along with this program; if not, write to the Free Software
00015 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301  USA.
00016 
00017 from bisect import bisect_left, bisect_right
00018 import collections
00019 import re
00020 import logging
00021 import sys
00022 from collections import namedtuple
00023 
00024 from gi.repository import GdkPixbuf
00025 from gi.repository import Gdk
00026 from gi.repository import Gtk
00027 from gi.repository import GLib
00028 from gi.repository import RB
00029 from gi.repository import GObject
00030 from gi.repository import Gio
00031 import lxml.etree as ET
00032 
00033 import rb
00034 from coverart_browser_prefs import CoverLocale
00035 from coverart_browser_prefs import GSetting
00036 import coverart_rb3compat as rb3compat
00037 from coverart_search_providers import lastfm_connected
00038 from coverart_search_providers import get_search_providers
00039 
00040 
00041 class FauxTb(object):
00042     def __init__(self, tb_frame, tb_lineno, tb_next):
00043         self.tb_frame = tb_frame
00044         self.tb_lineno = tb_lineno
00045         self.tb_next = tb_next
00046 
00047 
00048 def current_stack(skip=0):
00049     try:
00050         1 / 0
00051     except ZeroDivisionError:
00052         f = sys.exc_info()[2].tb_frame
00053     for i in range(skip + 2):
00054         f = f.f_back
00055     lst = []
00056     while f is not None:
00057         lst.append((f, f.f_lineno))
00058         f = f.f_back
00059     return lst
00060 
00061 
00062 def extend_traceback(tb, stack):
00063     """Extend traceback with stack info."""
00064     head = tb
00065     for tb_frame, tb_lineno in stack:
00066         head = FauxTb(tb_frame, tb_lineno, head)
00067     return head
00068 
00069 
00070 def full_exc_info():
00071     """Like sys.exc_info, but includes the full traceback."""
00072     t, v, tb = sys.exc_info()
00073     full_tb = extend_traceback(tb, current_stack(1))
00074     return t, v, full_tb
00075 
00076 
00077 def dumpstack(message):
00078     ''' dumps the current stack - useful of debugging
00079     '''
00080     logging.error(message, exc_info=full_exc_info())
00081 
00082 
00083 def uniquify_and_sort(iterable):
00084     ''' Removes duplicates of an iterables and returns a list of unique
00085     elements.
00086     '''
00087     uniques = []
00088 
00089     for element in iterable:
00090         if element not in uniques:
00091             uniques.append(element)
00092 
00093     return sorted(uniques)
00094 
00095 
00096 GenreType = namedtuple("GenreType", ["name", "genre_type"])
00097 
00098 
00099 class NaturalString(str):
00100     '''
00101     this class implements an object that can naturally compare
00102     strings
00103     i.e. "15 album" < "100 album"
00104     '''
00105 
00106     def __init__(self, string):
00107         super(NaturalString, self).__init__()
00108         convert = lambda text: int(text) if text.isdigit() else text.lower()
00109         alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)',
00110                                                                  key)]
00111 
00112         self._string_elements = alphanum_key(string)
00113 
00114     def __lt__(self, other):
00115         if type(other) is str:
00116             return super(NaturalString, self).__lt__(other)
00117         else:
00118             return self._string_elements < other._string_elements
00119 
00120     def __le__(self, other):
00121         if type(other) is str:
00122             return super(NaturalString, self).__le__(other)
00123         else:
00124             return self._string_elements <= other._string_elements
00125 
00126     def __gt__(self, other):
00127         if type(other) is str:
00128             return super(NaturalString, self).__gt__(other)
00129         else:
00130             return self._string_elements > other._string_elements
00131 
00132     def __ge__(self, other):
00133         if type(other) is str:
00134             return super(NaturalString, self).__ge__(other)
00135         else:
00136             return self._string_elements >= other._string_elements
00137 
00138 
00139 class SortedCollection(object):
00140     '''Sequence sorted by a key function.
00141 
00142     SortedCollection() is much easier to work with than using bisect() directly.
00143     It supports key functions like those use in sorted(), min(), and max().
00144     The result of the key function call is saved so that keys can be searched
00145     efficiently.
00146 
00147     Instead of returning an insertion-point which can be hard to interpret, the
00148     five find-methods return a specific item in the sequence. They can scan for
00149     exact matches, the last item less-than-or-equal to a key, or the first item
00150     greater-than-or-equal to a key.
00151 
00152     Once found, an item's ordinal position can be located with the index() method.
00153     New items can be added with the insert() and insert_right() methods.
00154     Old items can be deleted with the remove() method.
00155 
00156     The usual sequence methods are provided to support indexing, slicing,
00157     length lookup, clearing, copying, forward and reverse iteration, contains
00158     checking, item counts, item removal, and a nice looking repr.
00159 
00160     Finding and indexing are O(log n) operations while iteration and insertion
00161     are O(n).  The initial sort is O(n log n).
00162 
00163     The key function is stored in the 'key' attibute for easy introspection or
00164     so that you can assign a new key function (triggering an automatic re-sort).
00165 
00166     In short, the class was designed to handle all of the common use cases for
00167     bisect but with a simpler API and support for key functions.
00168 
00169     >>> from pprint import pprint
00170     >>> from operator import itemgetter
00171 
00172     >>> s = SortedCollection(key=itemgetter(2))
00173     >>> for record in [
00174     ...         ('roger', 'young', 30),
00175     ...         ('angela', 'jones', 28),
00176     ...         ('bill', 'smith', 22),
00177     ...         ('david', 'thomas', 32)]:
00178     ...     s.insert(record)
00179 
00180     >>> pprint(list(s))         # show records sorted by age
00181     [('bill', 'smith', 22),
00182      ('angela', 'jones', 28),
00183      ('roger', 'young', 30),
00184      ('david', 'thomas', 32)]
00185 
00186     >>> s.find_le(29)           # find oldest person aged 29 or younger
00187     ('angela', 'jones', 28)
00188     >>> s.find_lt(28)           # find oldest person under 28
00189     ('bill', 'smith', 22)
00190     >>> s.find_gt(28)           # find youngest person over 28
00191     ('roger', 'young', 30)
00192 
00193     >>> r = s.find_ge(32)       # find youngest person aged 32 or older
00194     >>> s.index(r)              # get the index of their record
00195     3
00196     >>> s[3]                    # fetch the record at that index
00197     ('david', 'thomas', 32)
00198 
00199     >>> s.key = itemgetter(0)   # now sort by first name
00200     >>> pprint(list(s))
00201     [('angela', 'jones', 28),
00202      ('bill', 'smith', 22),
00203      ('david', 'thomas', 32),
00204      ('roger', 'young', 30)]
00205 
00206     '''
00207 
00208     def __init__(self, iterable=(), key=None):
00209         self._given_key = key
00210         key = (lambda x: x) if key is None else key
00211         decorated = sorted((key(item), item) for item in iterable)
00212         self._keys = [k for k, item in decorated]
00213         self._items = [item for k, item in decorated]
00214         self._key = key
00215 
00216     def _getkey(self):
00217         return self._key
00218 
00219     def _setkey(self, key):
00220         if key is not self._key:
00221             self.__init__(self._items, key=key)
00222 
00223     def _delkey(self):
00224         self._setkey(None)
00225 
00226     key = property(_getkey, _setkey, _delkey, 'key function')
00227 
00228     def clear(self):
00229         self.__init__([], self._key)
00230 
00231     def copy(self):
00232         return self.__class__(self, self._key)
00233 
00234     def __len__(self):
00235         return len(self._items)
00236 
00237     def __getitem__(self, i):
00238         return self._items[i]
00239 
00240     def __iter__(self):
00241         return iter(self._items)
00242 
00243     def __reversed__(self):
00244         return ReversedSortedCollection(self)
00245 
00246     def __repr__(self):
00247         return '%s(%r, key=%s)' % (
00248             self.__class__.__name__,
00249             self._items,
00250             getattr(self._given_key, '__name__', repr(self._given_key))
00251         )
00252 
00253     def __reduce__(self):
00254         return self.__class__, (self._items, self._given_key)
00255 
00256     def __contains__(self, item):
00257         k = self._key(item)
00258         i = bisect_left(self._keys, k)
00259         j = bisect_right(self._keys, k)
00260         return item in self._items[i:j]
00261 
00262     def index(self, item):
00263         'Find the position of an item.  Raise ValueError if not found.'
00264         k = self._key(item)
00265         i = bisect_left(self._keys, k)
00266         j = bisect_right(self._keys, k)
00267         return self._items[i:j].index(item) + i
00268 
00269     def count(self, item):
00270         'Return number of occurrences of item'
00271         k = self._key(item)
00272         i = bisect_left(self._keys, k)
00273         j = bisect_right(self._keys, k)
00274         return self._items[i:j].count(item)
00275 
00276     def insert(self, item):
00277         'Insert a new item.  If equal keys are found, add to the left'
00278         k = self._key(item)
00279         i = bisect_left(self._keys, k)
00280         self._keys.insert(i, k)
00281         self._items.insert(i, item)
00282 
00283         return i
00284 
00285     def reorder(self, item):
00286         '''Reorder an item. If its key changed, then the item is
00287         repositioned, otherwise the item stays untouched'''
00288         index = self._items.index(item)
00289         new_index = -1
00290 
00291         if self._keys[index] != self._key(item):
00292             del self._keys[index]
00293             del self._items[index]
00294 
00295             new_index = self.insert(item)
00296 
00297         return new_index
00298 
00299     def insert_all(self, items):
00300         for item in items:
00301             self.insert(item)
00302 
00303     def remove(self, item):
00304         'Remove first occurence of item.  Raise ValueError if not found'
00305         i = self.index(item)
00306         del self._keys[i]
00307         del self._items[i]
00308 
00309 
00310 class ReversedSortedCollection(object):
00311     def __init__(self, sorted_collection):
00312         self._sorted_collection = sorted_collection
00313 
00314     def __getattr__(self, name):
00315         return getattr(self._sorted_collection, name)
00316 
00317     def copy(self):
00318         return self.__class__(self._sorted_collection)
00319 
00320     def _getkey(self):
00321         return self._key
00322 
00323     def _setkey(self, key):
00324         if key is not self._key:
00325             self.__init__(SortedCollection(self._items, key=key))
00326 
00327     def _delkey(self):
00328         self._setkey(None)
00329 
00330     key = property(_getkey, _setkey, _delkey, 'key function')
00331 
00332     def __len__(self):
00333         return len(self._sorted_collection)
00334 
00335     def __getitem__(self, i):
00336         return self._items[len(self) - i - 1]
00337 
00338     def __iter__(self):
00339         return iter(reversed(self._items))
00340 
00341     def __reversed__(self):
00342         return self._sorted_collection
00343 
00344     def __repr__(self):
00345         return '%s(%r, key=%s)' % (
00346             self.__class__.__name__,
00347             reversed(self._items),
00348             getattr(self._given_key, '__name__', repr(self._given_key))
00349         )
00350 
00351     def __reduce__(self):
00352         return self.__class__, (reversed(self._items), self._given_key)
00353 
00354     def insert(self, item):
00355         'Insert a new item.  If equal keys are found, add to the left'
00356         i = self._sorted_collection.insert(item)
00357 
00358         return len(self) - i - 1
00359 
00360     def index(self, item):
00361         'Find the position of an item.  Raise ValueError if not found.'
00362         return len(self) - self._sorted_collection.index(item) - 1
00363 
00364 
00365 class IdleCallIterator(object):
00366     def __init__(self, chunk, process, after=None, error=None, finish=None):
00367         default = lambda *_: None
00368 
00369         self._chunk = chunk
00370         self._process = process
00371         self._after = after if after else default
00372         self._error = error if error else default
00373         self._finish = finish if finish else default
00374         self._stop = False
00375 
00376     def __call__(self, iterator, **data):
00377         self._iter = iterator
00378 
00379         Gdk.threads_add_idle(GLib.PRIORITY_DEFAULT_IDLE, self._idle_call, data)
00380 
00381     def _idle_call(self, data):
00382         if self._stop:
00383             return False
00384 
00385         for i in range(self._chunk):
00386             try:
00387                 next_elem = next(self._iter)
00388 
00389                 self._process(next_elem, data)
00390             except StopIteration:
00391                 self._finish(data)
00392                 return False
00393             except Exception as e:
00394                 self._error(e)
00395 
00396         self._after(data)
00397 
00398         return True
00399 
00400     def stop(self):
00401         self._stop = True
00402 
00403 
00404 def idle_iterator(func):
00405     def iter_function(obj, iterator, **data):
00406         idle_call = IdleCallIterator(*func(obj))
00407 
00408         idle_call(iterator, **data)
00409 
00410         return idle_call
00411 
00412     return iter_function
00413 
00414 
00415 class Theme:
00416     '''
00417     This class manages the theme details
00418     '''
00419     # storage for the instance reference
00420     __instance = None
00421 
00422     class _impl(GObject.Object):
00423         """ Implementation of the singleton interface """
00424         # properties
00425         theme = GObject.property(type=str, default="standard")
00426 
00427         # signals
00428         '''
00429         changed = signal emitted when a theme has changed
00430         '''
00431         __gsignals__ = {
00432             'theme_changed': (GObject.SIGNAL_RUN_LAST, None, ())
00433         }
00434         # below public variables and methods that can be called for Theme
00435         def __init__(self, plugin):
00436             '''
00437             Initializes the singleton interface, assigning all the constants
00438             used to access the plugin's settings.
00439             '''
00440             super(Theme._impl, self).__init__()
00441 
00442             self.plugin = plugin
00443             popups = rb.find_plugin_file(plugin, 'img/popups.xml')
00444             root = ET.parse(open(popups)).getroot()
00445 
00446             base = 'theme/theme'
00447             self.themes = []
00448 
00449             for elem in root.xpath(base):
00450                 self.themes.append(elem.attrib['folder_name'])
00451 
00452             self.gs = GSetting()
00453             self.setting = self.gs.get_setting(self.gs.Path.PLUGIN)
00454 
00455             # connect properties and signals
00456             self._connect_properties()
00457             self._connect_signals()
00458 
00459         @property
00460         def current(self):
00461             return self.setting[self.gs.PluginKey.THEME]
00462 
00463         def _connect_properties(self):
00464             self.setting.bind(self.gs.PluginKey.THEME, self,
00465                               'theme', Gio.SettingsBindFlags.GET)
00466 
00467         def _connect_signals(self):
00468             self.connect('notify::theme', self._on_theme_changed,
00469                          None)
00470 
00471         def _on_theme_changed(self, *args):
00472             self.emit('theme_changed')
00473 
00474     def __init__(self, plugin):
00475         """ Create singleton instance """
00476         # Check whether we already have an instance
00477         if Theme.__instance is None:
00478             # Create and remember instance
00479             Theme.__instance = Theme._impl(plugin)
00480 
00481         # Store instance reference as the only member in the handle
00482         self.__dict__['_Theme__instance'] = Theme.__instance
00483 
00484     def __getattr__(self, attr):
00485         """ Delegate access to implementation """
00486         return getattr(self.__instance, attr)
00487 
00488     def __setattr__(self, attr, value):
00489         """ Delegate access to implementation """
00490         return setattr(self.__instance, attr, value)
00491 
00492 
00493 class SpriteSheet(object):
00494     def __init__(self, image, icon_width, icon_height, x_spacing, y_spacing,
00495                  x_start, y_start, across_dimension, down_dimension,
00496                  alpha_color=None, size=None):
00497         # load the image
00498         base_image = GdkPixbuf.Pixbuf.new_from_file(image)
00499 
00500         if alpha_color:
00501             base_image = base_image.add_alpha(True, *alpha_color)
00502 
00503         delta_y = icon_height + y_spacing
00504         delta_x = icon_width + x_spacing
00505 
00506         self._sprites = []
00507 
00508         for y in range(0, down_dimension):
00509             for x in range(0, across_dimension):
00510                 sprite = GdkPixbuf.Pixbuf.new(GdkPixbuf.Colorspace.RGB, True,
00511                                               8, icon_width, icon_height)
00512 
00513                 base_image.copy_area(x_start + (x * delta_x),
00514                                      y_start + (y * delta_y), icon_width, icon_height,
00515                                      sprite, 0, 0)
00516 
00517                 if size:
00518                     sprite = sprite.scale_simple(size[0], size[1],
00519                                                  GdkPixbuf.InterpType.BILINEAR)
00520 
00521                 self._sprites.append(sprite)
00522 
00523     def __len__(self):
00524         return len(self._sprites)
00525 
00526     def __getitem__(self, index):
00527         return self._sprites[index]
00528 
00529 
00530 class ConfiguredSpriteSheet(object):
00531     def __init__(self, plugin, sprite_name, size=None):
00532         popups = rb.find_plugin_file(plugin, 'img/popups.xml')
00533         root = ET.parse(open(popups)).getroot()
00534         base = 'theme/theme[@folder_name="' + Theme(plugin).current \
00535                + '"]/spritesheet[@name="' + sprite_name + '"]/'
00536         image = rb.find_plugin_file(plugin, 'img/' + Theme(plugin).current \
00537                                             + '/' + root.xpath(base + 'image')[0].text)
00538         icon_width = int(root.xpath(base + 'icon')[0].attrib['width'])
00539         icon_height = int(root.xpath(base + 'icon')[0].attrib['height'])
00540         x_spacing = int(root.xpath(base + 'spacing')[0].attrib['x'])
00541         y_spacing = int(root.xpath(base + 'spacing')[0].attrib['y'])
00542         x_start = int(root.xpath(base + 'start-position')[0].attrib['x'])
00543         y_start = int(root.xpath(base + 'start-position')[0].attrib['y'])
00544         across_dimension = int(root.xpath(base + 'dimension')[0].attrib['across'])
00545         down_dimension = int(root.xpath(base + 'dimension')[0].attrib['down'])
00546 
00547         try:
00548             alpha_color = list(map(int,
00549                                    root.xpath(base + 'alpha')[0].text.split(' ')))
00550         except:
00551             alpha_color = None
00552 
00553         self.names = []
00554         self.locale_names = {}
00555 
00556         cl = CoverLocale()
00557         lang = cl.get_locale()
00558 
00559         base = sprite_name + '/' + sprite_name + \
00560                '[@spritesheet="' + sprite_name + '"]'
00561 
00562         for elem in root.xpath(base + '[not(@xml:lang)]'):
00563             self.names.append(elem.text)
00564 
00565         for elem in root.xpath(base + '[@xml:lang="' + lang + '"]'):
00566             self.locale_names[elem.text] = elem.attrib['name']
00567 
00568         if (not self.locale_names) and len(lang) > 2:
00569             for elem in root.xpath(base + '[@xml:lang="' + \
00570                     lang[0:2] + '"]'):
00571                 self.locale_names[elem.text] = elem.attrib['name']
00572 
00573         self._sheet = SpriteSheet(image, icon_width, icon_height, x_spacing,
00574                                   y_spacing, x_start, y_start, across_dimension, down_dimension,
00575                                   alpha_color, size)
00576 
00577         self._genre_db = RB.ExtDB(name='cb_genre')
00578 
00579     def __len__(self):
00580         return len(self._sheet)
00581 
00582     def __getitem__(self, name):
00583         try:
00584             return self._sheet[self.names.index(name)]
00585         except:
00586             return None
00587 
00588     def __contains__(self, name):
00589         return name in self.names
00590 
00591     def keys(self):
00592         return self.names
00593 
00594 
00595 class GenreConfiguredSpriteSheet(ConfiguredSpriteSheet):
00596     '''
00597     A sprite-sheet of genres. Creates a pixbuf representation of a picture
00598     that has several icons in a regular pattern.  This uses the file
00599     'popups.xml' for its definition
00600 
00601     :plugin: rhythmbox plugin
00602     :sprite_name: `str` containing name of the spritesheet pattern in
00603       popups.xml
00604     :size: `int` array dimension of the final sprite which is to be used.
00605 
00606     output:
00607     :names: `str` array  of sprite names
00608     '''
00609     # types of genre
00610     GENRE_USER = 1
00611     GENRE_SYSTEM = 2
00612     GENRE_LOCALE = 3
00613 
00614     def __init__(self, plugin, sprite_name, size=None):
00615         super(GenreConfiguredSpriteSheet, self).__init__(plugin, sprite_name,
00616                                                          size)
00617         self.genre_alternate = {}  # contains GenreType tuples
00618         self._alt_icons = {}
00619         self._sprite_name = sprite_name
00620         self._size = size
00621 
00622         popups = rb.find_plugin_file(plugin, 'img/popups.xml')
00623         root = ET.parse(open(popups)).getroot()
00624         self._parse_popups(plugin, root, self.GENRE_SYSTEM)
00625 
00626         try:
00627             #self._user_popups = RB.find_user_data_file('plugins/coverart_browser/img/usericons/popups.xml')
00628             self._user_popups = RB.user_cache_dir() + "/coverart_browser/usericons/popups.xml"
00629             root = ET.parse(open(self._user_popups)).getroot()
00630             self._parse_popups(plugin, root, self.GENRE_USER)
00631             elem = root.xpath(self._sprite_name + '/index')
00632             curr_index = int(elem[0].text)
00633 
00634             for index in range(0, curr_index + 1):
00635                 key = RB.ExtDBKey.create_lookup('icon', str(index))
00636                 icon_location = self._genre_db.lookup(key)
00637                 sprite = GdkPixbuf.Pixbuf.new_from_file(icon_location)
00638                 if self._size:
00639                     sprite = sprite.scale_simple(self._size[0], self._size[1],
00640                                                  GdkPixbuf.InterpType.BILINEAR)
00641 
00642                 self._alt_icons[str(index)] = sprite
00643                 self.names.append(str(index))
00644         except:
00645             pass
00646 
00647     def __getitem__(self, name):
00648         try:
00649             return self._alt_icons[name]
00650         except:
00651             return self._sheet[self.names.index(name)]
00652 
00653     def _parse_popups(self, plugin, root, genre_type):
00654         icon_names = {}
00655         cl = CoverLocale()
00656         lang = cl.get_locale()
00657 
00658         base = self._sprite_name + '/alt'
00659         for elem in root.xpath(base + '[not(@xml:lang)]/alt'):
00660             self.genre_alternate[GenreType(name=elem.text, genre_type=genre_type)] = elem.attrib['genre']
00661 
00662         for elem in root.xpath(base + '[@xml:lang="' + lang + '"]/alt'):
00663             self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_LOCALE)] = elem.attrib['genre']
00664 
00665         # if (not self.locale_alternate) and len(lang) > 2:
00666         if len(lang) > 2:
00667             for elem in root.xpath(base + '[@xml:lang="' + \
00668                     lang[0:2] + '"]/alt'):
00669                 self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_LOCALE)] = elem.attrib['genre']
00670 
00671     def add_genre_icon(self, filename):
00672         root = ET.parse(open(self._user_popups)).getroot()
00673         elem = root.xpath(self._sprite_name + '/index')
00674         next_index = int(elem[0].text)
00675         elem[0].text = str(next_index + 1)
00676         tree = ET.ElementTree(root)
00677         tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
00678 
00679         key = RB.ExtDBKey.create_storage('icon', str(next_index))
00680         uri = "file://" + rb3compat.pathname2url(filename)
00681 
00682         self._genre_db.store_uri(key, RB.ExtDBSourceType.USER_EXPLICIT, uri)
00683 
00684         pixbuf = GdkPixbuf.Pixbuf.new_from_file(filename)
00685         new_genre = GenreType(name=str(next_index), genre_type=self.GENRE_USER)
00686 
00687         if self._size:
00688             pixbuf = pixbuf.scale_simple(self._size[0], self._size[1],
00689                                          GdkPixbuf.InterpType.BILINEAR)
00690 
00691         self._alt_icons[new_genre.name] = pixbuf
00692         self.names.append(new_genre.name)
00693 
00694         return new_genre
00695 
00696     def delete_genre(self, current_genre):
00697         root = ET.parse(open(self._user_popups)).getroot()
00698         base = self._sprite_name + '/alt/alt'
00699 
00700         found = False
00701 
00702         for elem in root.xpath(base):
00703             if RB.search_fold(elem.text) == RB.search_fold(current_genre):
00704                 found = True
00705                 break
00706 
00707         if found:
00708             elem.getparent().remove(elem)
00709             tree = ET.ElementTree(root)
00710             tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
00711         else:
00712             print("not found to delete")
00713 
00714 
00715     def amend_genre_info(self, current_genre, new_genre, icon_name):
00716         root = ET.parse(open(self._user_popups)).getroot()
00717         base = self._sprite_name + '/alt/alt'
00718 
00719         found = False
00720 
00721         if current_genre != "":
00722             for elem in root.xpath(base):
00723                 if RB.search_fold(elem.text) == RB.search_fold(current_genre):
00724                     found = True
00725                     del self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_USER)]
00726                     break
00727 
00728         else:
00729             elem = ET.SubElement(root.xpath(self._sprite_name + '/alt')[0], "alt")
00730             if elem != None:
00731                 found = True
00732 
00733         if found:
00734             elem.text = rb3compat.unicodestr(new_genre, 'utf-8')
00735             elem.attrib['genre'] = icon_name
00736 
00737             tree = ET.ElementTree(root)
00738             tree.write(self._user_popups, pretty_print=True, xml_declaration=True)
00739             self.genre_alternate[GenreType(name=elem.text, genre_type=self.GENRE_USER)] = icon_name
00740             return GenreType(name=elem.text, genre_type=self.GENRE_USER)
00741         else:
00742             print("nothing found to amend")
00743             return None
00744 
00745 
00746 def get_stock_size():
00747     what, width, height = Gtk.icon_size_lookup(Gtk.IconSize.BUTTON)
00748 
00749     return width, height
00750 
00751 
00752 def create_pixbuf_from_file_at_size(filename, width, height):
00753     pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_size(filename, width, height)
00754 
00755     if pixbuf.get_width() != width or pixbuf.get_height() != height:
00756         pixbuf = pixbuf.scale_simple(width, height,
00757                                      GdkPixbuf.InterpType.BILINEAR)
00758 
00759     return pixbuf
00760 
00761 
00762 '''
00763 class to search through a dict without case-sensitivity nor
00764 unicode vs string issues
00765 '''
00766 
00767 
00768 class CaseInsensitiveDict(collections.Mapping):
00769     def __init__(self, d):
00770         self._d = d
00771         self._s = dict((RB.search_fold(k), k) for k in d)
00772 
00773     def __contains__(self, k):
00774         return RB.search_fold(k) in self._s
00775 
00776     def __len__(self):
00777         return len(self._s)
00778 
00779     def __iter__(self):
00780         return iter(self._s)
00781 
00782     def __getitem__(self, k):
00783         return self._d[self._s[RB.search_fold(k)]]
00784 
00785     def actual_key_case(self, k):
00786         return self._s.get(RB.search_fold(k))
00787 
00788 
00789 def check_lastfm(force_check=False):
00790     '''
00791     check validity of lastfm connection
00792     
00793     returns True if connected with an account 
00794     
00795     Also returns True if lastFM is not in the list of search providers
00796     '''
00797 
00798     providers = get_search_providers()
00799     print(providers)
00800     print(force_check)
00801 
00802     if force_check or 'lastfm-search' in providers:
00803         connected = lastfm_connected()
00804         print(connected)
00805         return connected
00806     elif not 'lastfm-search' in providers:
00807         print("not lastm-search")
00808         return True
00809     else:
00810         print("returning default")
00811         return False
00812 
00813 
00814 def create_button_image(plugin, icon_name):
00815     'create a pixbuf for the given icon_name sized according to the stock icon size'
00816     path = 'img/'
00817 
00818     return create_pixbuf_from_file_at_size(
00819         rb.find_plugin_file(plugin, path + icon_name),
00820         *get_stock_size())
 All Classes Functions