CoverArt Browser
v2.0
Browse your cover-art albums in Rhythmbox
|
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())