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 # Copyright (C) 2012 - fossfreedom 00004 # Copyright (C) 2012 - Agustin Carrasco 00005 # 00006 # This program is free software; you can redistribute it and/or modify 00007 # it under the terms of the GNU General Public License as published by 00008 # the Free Software Foundation; either version 2, or (at your option) 00009 # any later version. 00010 # 00011 # This program is distributed in the hope that it will be useful, 00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 # GNU General Public License for more details. 00015 # 00016 # You should have received a copy of the GNU General Public License 00017 # along with this program; if not, write to the Free Software 00018 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 00019 00020 import os 00021 import tempfile 00022 import shutil 00023 00024 from gi.repository import Gdk 00025 from gi.repository import Gtk 00026 from gi.repository import GLib 00027 from gi.repository import GObject 00028 from gi.repository import Gio 00029 from gi.repository import GdkPixbuf 00030 from gi.repository import RB 00031 00032 from coverart_browser_prefs import GSetting 00033 from coverart_album import Album 00034 from coverart_album import AlbumsModel 00035 from coverart_album import CoverManager 00036 from coverart_widgets import AbstractView 00037 from coverart_utils import SortedCollection 00038 from coverart_widgets import PanedCollapsible 00039 from coverart_toolbar import ToolbarObject 00040 from coverart_utils import idle_iterator 00041 from coverart_utils import dumpstack 00042 from coverart_utils import create_pixbuf_from_file_at_size 00043 from coverart_extdb import CoverArtExtDB 00044 import coverart_rb3compat as rb3compat 00045 from coverart_rb3compat import Menu 00046 import rb 00047 00048 00049 def create_temporary_copy(path): 00050 temp_dir = tempfile.gettempdir() 00051 filename = tempfile.mktemp() 00052 temp_path = os.path.join(temp_dir, filename) 00053 shutil.copy2(path, temp_path) 00054 return temp_path 00055 00056 00057 ARTIST_LOAD_CHUNK = 50 00058 00059 00060 class Artist(GObject.Object): 00061 ''' 00062 An album. It's conformed from one or more tracks, and many of it's 00063 information is deduced from them. 00064 00065 :param name: `str` name of the artist. 00066 :param cover: `Cover` cover for this artist. 00067 ''' 00068 # signals 00069 __gsignals__ = { 00070 'modified': (GObject.SIGNAL_RUN_FIRST, None, ()), 00071 'emptied': (GObject.SIGNAL_RUN_LAST, None, ()), 00072 'cover-updated': (GObject.SIGNAL_RUN_LAST, None, ()) 00073 } 00074 00075 __hash__ = GObject.__hash__ 00076 00077 def __init__(self, name, cover): 00078 super(Artist, self).__init__() 00079 00080 self.name = name 00081 self._cover = None 00082 self.cover = cover 00083 00084 self._signals_id = {} 00085 00086 @property 00087 def cover(self): 00088 return self._cover 00089 00090 @cover.setter 00091 def cover(self, new_cover): 00092 #if self._cover: 00093 # self._cover.disconnect(self._cover_resized_id) 00094 00095 self._cover = new_cover 00096 #self._cover_resized_id = self._cover.connect('resized', 00097 # lambda *args: self.emit('cover-updated')) 00098 00099 self.emit('cover-updated') 00100 00101 def create_ext_db_key(self): 00102 ''' 00103 Returns an `RB.ExtDBKey` 00104 ''' 00105 key = RB.ExtDBKey.create_lookup('artist', self.name) 00106 return key 00107 00108 00109 class ArtistsModel(GObject.Object): 00110 ''' 00111 Model that contains artists, keeps them sorted, filtered and provides an 00112 external `Gtk.TreeModel` interface to use as part of a Gtk interface. 00113 00114 The `Gtk.TreeModel` haves the following structure: 00115 column 0 -> string containing the artist name 00116 column 1 -> pixbuf of the artist's cover. 00117 column 2 -> instance of the artist or album itself. 00118 column 3 -> boolean that indicates if the row should be shown 00119 column 4 -> blank text column to pad the view correctly 00120 column 5 -> markup containing formatted text 00121 column 6 -> blank text for the expander column 00122 ''' 00123 # signals 00124 __gsignals__ = { 00125 'update-path': (GObject.SIGNAL_RUN_LAST, None, (object,)), 00126 'visual-updated': ((GObject.SIGNAL_RUN_LAST, None, (object, object))) 00127 } 00128 00129 # list of columns names and positions on the TreeModel 00130 columns = {'tooltip': 0, 'pixbuf': 1, 00131 'artist_album': 2, 'show': 3, 00132 'empty': 4, 'markup': 5, 'expander': 6} 00133 00134 def __init__(self, album_manager): 00135 super(ArtistsModel, self).__init__() 00136 00137 self.album_manager = album_manager 00138 self._iters = {} 00139 self._albumiters = {} 00140 self._artists = SortedCollection( 00141 key=lambda artist: getattr(artist, 'name')) 00142 00143 self._tree_store = Gtk.TreeStore(str, GdkPixbuf.Pixbuf, object, 00144 bool, str, str, str) 00145 00146 # sorting idle call 00147 self._sort_process = None 00148 00149 # create the filtered store that's used with the view 00150 self._filtered_store = self._tree_store.filter_new() 00151 self._filtered_store.set_visible_column(ArtistsModel.columns['show']) 00152 00153 self._tree_sort = Gtk.TreeModelSort(model=self._filtered_store) 00154 #self._tree_sort.set_default_sort_func(lambda *unused: 0) 00155 self._tree_sort.set_sort_func(0, self._compare, None) 00156 00157 self._connect_signals() 00158 00159 def _connect_signals(self): 00160 self.connect('update-path', self._on_update_path) 00161 self.album_manager.model.connect('filter-changed', self._on_album_filter_changed) 00162 00163 def _on_album_filter_changed(self, *args): 00164 if len(self._iters) == 0: 00165 return 00166 00167 artists = list(set(row[AlbumsModel.columns['album']].artist for row in self.album_manager.model.store)) 00168 00169 for artist in self._iters: 00170 self.show(artist, artist in artists) 00171 00172 def _compare(self, model, row1, row2, user_data): 00173 00174 if not model.iter_has_child(row1) or \ 00175 not model.iter_has_child(row2): 00176 return 0 00177 00178 sort_column = 0 00179 value1 = RB.search_fold(model.get_value(row1, sort_column)) 00180 value2 = RB.search_fold(model.get_value(row2, sort_column)) 00181 if value1 < value2: 00182 return -1 00183 elif value1 == value2: 00184 return 0 00185 else: 00186 return 1 00187 00188 @property 00189 def store(self): 00190 #return self._filtered_store 00191 return self._tree_sort 00192 00193 def add(self, artist): 00194 ''' 00195 Add an artist to the model. 00196 00197 :param artist: `Artist` to be added to the model. 00198 ''' 00199 # generate necessary values 00200 values = self._generate_artist_values(artist) 00201 # insert the values 00202 pos = self._artists.insert(artist) 00203 tree_iter = self._tree_store.insert(None, pos, values) 00204 child_iter = self._tree_store.insert(tree_iter, pos, values) # dummy child row so that the expand is available 00205 # connect signals 00206 ids = (artist.connect('modified', self._artist_modified), 00207 artist.connect('cover-updated', self._cover_updated), 00208 artist.connect('emptied', self.remove)) 00209 00210 if not artist.name in self._iters: 00211 self._iters[artist.name] = {} 00212 self._iters[artist.name] = {'artist_album': artist, 00213 'iter': tree_iter, 'dummy_iter': child_iter, 'ids': ids} 00214 return tree_iter 00215 00216 def _emit_signal(self, tree_iter, signal): 00217 # we get the filtered path and iter since that's what the outside world 00218 # interacts with 00219 tree_path = self._filtered_store.convert_child_path_to_path( 00220 self._tree_store.get_path(tree_iter)) 00221 00222 if tree_path: 00223 # if there's no path, the album doesn't show on the filtered model 00224 # so no one needs to know 00225 tree_iter = self._filtered_store.get_iter(tree_path) 00226 00227 self.emit(signal, tree_path, tree_iter) 00228 00229 def remove(self, *args): 00230 print("artist remove") 00231 00232 def _cover_updated(self, artist): 00233 tree_iter = self._iters[artist.name]['iter'] 00234 00235 if self._tree_store.iter_is_valid(tree_iter): 00236 # only update if the iter is valid 00237 pixbuf = artist.cover.pixbuf 00238 00239 self._tree_store.set_value(tree_iter, self.columns['pixbuf'], 00240 pixbuf) 00241 00242 self._emit_signal(tree_iter, 'visual-updated') 00243 00244 def _artist_modified(self, *args): 00245 print("artist modified") 00246 00247 def _on_update_path(self, widget, treepath): 00248 ''' 00249 called when update-path signal is called 00250 ''' 00251 artist = self.get_from_path(treepath) 00252 albums = self.album_manager.model.get_all() 00253 self.add_album_to_artist(artist, albums) 00254 00255 def add_album_to_artist(self, artist, albums): 00256 ''' 00257 Add an album to the artist in the model. 00258 00259 :param artist: `Artist` for the album to be added to (i.e. the parent) 00260 :param album: array of `Album` which are the children of the Artist 00261 00262 ''' 00263 # get the artist iter 00264 artist_iter = self._iters[artist.name]['iter'] 00265 00266 # now remove the dummy_iter - if this fails, we've removed this 00267 # before and have no need to add albums 00268 00269 if 'dummy_iter' in self._iters[artist.name]: 00270 self._iters[artist.name]['album'] = [] 00271 00272 for album in albums: 00273 if artist.name == album.artist and not (album in self._albumiters): 00274 # now for all matching albums that were found lets add to the model 00275 00276 # generate necessary values 00277 values = self._generate_album_values(album) 00278 # insert the values 00279 tree_iter = self._tree_store.append(artist_iter, values) 00280 self._albumiters[album] = {} 00281 self._albumiters[album]['iter'] = tree_iter 00282 self._iters[artist.name]['album'].append(tree_iter) 00283 00284 # connect signals 00285 ids = (album.connect('modified', self._album_modified), 00286 album.connect('cover-updated', self._album_coverupdate), 00287 album.connect('emptied', self._album_emptied)) 00288 00289 self._albumiters[album]['ids'] = ids 00290 00291 if 'dummy_iter' in self._iters[artist.name]: 00292 self._tree_store.remove(self._iters[artist.name]['dummy_iter']) 00293 del self._iters[artist.name]['dummy_iter'] 00294 00295 self.sort() # ensure the added albums are sorted correctly 00296 00297 def _album_modified(self, album): 00298 print("album modified") 00299 print(album) 00300 if not (album in self._albumiters): 00301 print("not found in albumiters") 00302 return 00303 00304 tree_iter = self._albumiters[album]['iter'] 00305 00306 if self._tree_store.iter_is_valid(tree_iter): 00307 # only update if the iter is valid 00308 # generate and update values 00309 tooltip, pixbuf, album, show, blank, markup, empty = \ 00310 self._generate_album_values(album) 00311 00312 self._tree_store.set(tree_iter, self.columns['tooltip'], tooltip, 00313 self.columns['markup'], markup, self.columns['show'], show) 00314 00315 self.sort() # ensure the added albums are sorted correctly 00316 00317 def _album_emptied(self, album): 00318 ''' 00319 Removes this album from the model. 00320 00321 :param album: `Album` to be removed from the model. 00322 ''' 00323 print('album emptied') 00324 print(album) 00325 print(album.artist) 00326 if not (album in self._albumiters): 00327 print("not found in albumiters") 00328 return 00329 00330 artist = self.get(album.artist) 00331 album_iter = self._albumiters[album]['iter'] 00332 00333 self._iters[album.artist]['album'].remove(album_iter) 00334 self._tree_store.remove(album_iter) 00335 00336 # disconnect signals 00337 for sig_id in self._albumiters[album]['ids']: 00338 album.disconnect(sig_id) 00339 00340 del self._albumiters[album] 00341 00342 # test if there are any more albums for this artist otherwise just cleanup 00343 if len(self._iters[album.artist]['album']) == 0: 00344 self.remove(artist) 00345 self._on_album_filter_changed(_) 00346 00347 def _album_coverupdate(self, album): 00348 tooltip, pixbuf, album, show, blank, markup, empty = self._generate_album_values(album) 00349 self._tree_store.set_value(self._albumiters[album]['iter'], 00350 self.columns['pixbuf'], pixbuf) 00351 00352 def _generate_artist_values(self, artist): 00353 tooltip = artist.name 00354 pixbuf = artist.cover.pixbuf 00355 show = True 00356 00357 return tooltip, pixbuf, artist, show, '', \ 00358 GLib.markup_escape_text(tooltip), '' 00359 00360 def _generate_album_values(self, album): 00361 tooltip = album.name 00362 pixbuf = album.cover.pixbuf.scale_simple(48, 48, GdkPixbuf.InterpType.BILINEAR) 00363 show = True 00364 00365 rating = album.rating 00366 if int(rating) > 0: 00367 rating = u'\u2605' * int(rating) 00368 else: 00369 rating = '' 00370 00371 year = ' (' + str(album.real_year) + ')' 00372 00373 track_count = album.track_count 00374 if track_count == 1: 00375 detail = rb3compat.unicodedecode(_(' with 1 track'), 'UTF-8') 00376 else: 00377 detail = rb3compat.unicodedecode(_(' with %d tracks') % 00378 track_count, 'UTF-8') 00379 00380 duration = album.duration / 60 00381 00382 if duration == 1: 00383 detail += rb3compat.unicodedecode(_(' and a duration of 1 minute'), 'UTF-8') 00384 else: 00385 detail += rb3compat.unicodedecode(_(' and a duration of %d minutes') % 00386 duration, 'UTF-8') 00387 00388 tooltip = rb3compat.unicodestr(tooltip, 'utf-8') 00389 tooltip = rb3compat.unicodeencode(tooltip, 'utf-8') 00390 import cgi 00391 00392 formatted = '<b><i>' + \ 00393 cgi.escape(rb3compat.unicodedecode(tooltip, 'utf-8')) + \ 00394 '</i></b>' + \ 00395 year + \ 00396 ' ' + rating + \ 00397 '\n<small>' + \ 00398 GLib.markup_escape_text(detail) + \ 00399 '</small>' 00400 00401 return tooltip, pixbuf, album, show, '', formatted, '' 00402 00403 def remove(self, artist): 00404 ''' 00405 Removes this artist from the model. 00406 00407 :param artist: `Artist` to be removed from the model. 00408 ''' 00409 self._artists.remove(artist) 00410 self._tree_store.remove(self._iters[artist.name]['iter']) 00411 00412 del self._iters[artist.name] 00413 00414 def contains(self, artist_name): 00415 ''' 00416 Indicates if the model contains a specific artist. 00417 00418 :param artist_name: `str` name of the artist. 00419 ''' 00420 return artist_name in self._iters 00421 00422 def get(self, artist_name): 00423 ''' 00424 Returns the requested Artist. 00425 00426 :param artist_name: `str` name of the artist. 00427 ''' 00428 return self._iters[artist_name]['artist_album'] 00429 00430 def get_albums(self, artist_name): 00431 ''' 00432 Returns the displayed albums for the requested artist 00433 00434 :param artist_name: `str` name of the artist. 00435 ''' 00436 00437 albums = [] 00438 00439 artist_iter = self._iters[artist_name]['iter'] 00440 next_iter = self._tree_store.iter_children(artist_iter) 00441 00442 while next_iter != None: 00443 albums.append(self._tree_store[next_iter][self.columns['artist_album']]) 00444 next_iter = self._tree_store.iter_next(next_iter) 00445 #if 'album' in self._iters[artist_name]: 00446 # for album_iter in self._iters[artist_name]['album']: 00447 # path = self._tree_store.get_path(album_iter) 00448 # if path: 00449 # tree_path = self._filtered_store.convert_child_path_to_path( 00450 # ) 00451 # albums.append(self.get_from_path(tree_path)) 00452 00453 return albums 00454 00455 def get_all(self): 00456 ''' 00457 Returns a collection of all the artists in this model. 00458 ''' 00459 return self._artists 00460 00461 def get_from_path(self, path): 00462 ''' 00463 Returns the Artist or Album referenced by a `Gtk.TreeModelSort` path. 00464 00465 :param path: `Gtk.TreePath` referencing the artist. 00466 ''' 00467 return self.store[path][self.columns['artist_album']] 00468 00469 def get_path(self, artist): 00470 print(artist.name) 00471 print(self._iters[artist.name]['iter']) 00472 return self._tree_store.get_path( 00473 self._iters[artist.name]['iter']) 00474 00475 def get_from_ext_db_key(self, key): 00476 ''' 00477 Returns the requested artist. 00478 00479 :param key: ext_db_key 00480 ''' 00481 # get the album name and artist 00482 name = key.get_field('artist') 00483 00484 # first check if there's a direct match 00485 artist = self.get(name) if self.contains(name) else None 00486 return artist 00487 00488 def show(self, artist_name, show): 00489 ''' 00490 filters/unfilters an artist, making it visible to the publicly available model's 00491 `Gtk.TreeModel` 00492 00493 :param artist: str containing the name of the artist to show or hide. 00494 :param show: `bool` indcating whether to show(True) or hide(False) the 00495 artist. 00496 ''' 00497 artist_iter = self._iters[artist_name]['iter'] 00498 00499 if self._tree_store.iter_is_valid(artist_iter): 00500 self._tree_store.set_value(artist_iter, self.columns['show'], show) 00501 00502 00503 def sort(self): 00504 00505 albums = SortedCollection(key=lambda album: getattr(album, 'name')) 00506 00507 gs = GSetting() 00508 source_settings = gs.get_setting(gs.Path.PLUGIN) 00509 key = source_settings[gs.PluginKey.SORT_BY_ARTIST] 00510 order = source_settings[gs.PluginKey.SORT_ORDER_ARTIST] 00511 00512 sort_keys = { 00513 'name_artist': ('album_sort', 'album_sort'), 00514 'year_artist': ('real_year', 'calc_year_sort'), 00515 'rating_artist': ('rating', 'album_sort') 00516 } 00517 00518 props = sort_keys[key] 00519 00520 def key_function(album): 00521 keys = [getattr(album, prop) for prop in props] 00522 return keys 00523 00524 # remember the current sort then remove the sort order 00525 # because sorting will only work in unsorted lists 00526 sortSettings = self.store.get_sort_column_id() 00527 00528 self.store.set_sort_column_id(-1, Gtk.SortType.ASCENDING) 00529 00530 for artist in self._iters: 00531 albums.clear() 00532 albums.key = key_function 00533 00534 if 'album' in self._iters[artist] and len(self._iters[artist]['album']) > 1: 00535 # we only need to sort an artists albums if there is more than one album 00536 00537 # sort all the artists albums 00538 for album_iter in self._iters[artist]['album']: 00539 albums.insert(self._tree_store[album_iter][self.columns['artist_album']]) 00540 00541 if not order: 00542 albums = reversed(albums) 00543 00544 # now we iterate through the sorted artist albums. Look and swap iters 00545 # according to where they are in the tree store 00546 00547 artist_iter = self._iters[artist]['iter'] 00548 next_iter = self._tree_store.iter_children(artist_iter) 00549 00550 for album in albums: 00551 if self._tree_store[next_iter][self.columns['artist_album']] != album: 00552 self._tree_store.swap(next_iter, self._albumiters[album]['iter']) 00553 next_iter = self._albumiters[album]['iter'] 00554 next_iter = self._tree_store.iter_next(next_iter) 00555 00556 # now we have finished sorting, reapply the sort 00557 self.store.set_sort_column_id(*sortSettings) 00558 00559 00560 class ArtistCellRenderer(Gtk.CellRendererPixbuf): 00561 def __init__(self): 00562 super(ArtistCellRenderer, self).__init__() 00563 00564 def do_render(self, cr, widget, 00565 background_area, 00566 cell_area, 00567 flags): 00568 newpix = self.props.pixbuf #.copy() 00569 #newpix = newpix.scale_simple(48,48,GdkPixbuf.InterpType.BILINEAR) 00570 00571 Gdk.cairo_set_source_pixbuf(cr, newpix, 0, 0) 00572 cr.paint() 00573 00574 00575 class ArtistLoader(GObject.Object): 00576 ''' 00577 Loads Artists - updating the model accordingly. 00578 00579 :param artist_manager: `artist_manager` responsible for this loader. 00580 ''' 00581 # signals 00582 __gsignals__ = { 00583 'artists-load-finished': (GObject.SIGNAL_RUN_LAST, None, (object,)), 00584 'model-load-finished': (GObject.SIGNAL_RUN_LAST, None, ()) 00585 } 00586 00587 def __init__(self, artist_manager, album_manager): 00588 super(ArtistLoader, self).__init__() 00589 00590 self.shell = artist_manager.shell 00591 self._connect_signals() 00592 self._album_manager = album_manager 00593 self._artist_manager = artist_manager 00594 00595 self.model = artist_manager.model 00596 00597 def load_artists(self): 00598 print ("load_artists") 00599 albums = self._album_manager.model.get_all() 00600 model = list(set(album.artist for album in albums)) 00601 00602 self._load_artists(iter(model), artists={}, model=model, 00603 total=len(model), progress=0.) 00604 00605 @idle_iterator 00606 def _load_artists(self): 00607 def process(row, data): 00608 # allocate the artist 00609 artist = Artist(row, self._artist_manager.cover_man.unknown_cover) 00610 00611 data['artists'][row] = artist 00612 00613 def after(data): 00614 # update the progress 00615 data['progress'] += ARTIST_LOAD_CHUNK 00616 00617 self._album_manager.progress = data['progress'] / data['total'] 00618 00619 def error(exception): 00620 print('Error processing entries: ' + str(exception)) 00621 00622 def finish(data): 00623 self._album_manager.progress = 1 00624 self.emit('artists-load-finished', data['artists']) 00625 00626 return ARTIST_LOAD_CHUNK, process, after, error, finish 00627 00628 @idle_iterator 00629 def _load_model(self): 00630 def process(artist, data): 00631 # add the artists to the model 00632 self._artist_manager.model.add(artist) 00633 00634 def after(data): 00635 data['progress'] += ARTIST_LOAD_CHUNK 00636 00637 # update the progress 00638 self._album_manager.progress = 1 - data['progress'] / data['total'] 00639 00640 def error(exception): 00641 dumpstack("Something awful happened!") 00642 print('Error(2) while adding artists to the model: ' + str(exception)) 00643 00644 def finish(data): 00645 self._album_manager.progress = 1 00646 self.emit('model-load-finished') 00647 print ("finished") 00648 #return False 00649 00650 return ARTIST_LOAD_CHUNK, process, after, error, finish 00651 00652 def _connect_signals(self): 00653 # connect signals for updating the albums 00654 #self.entry_changed_id = self._album_manager.db.connect('entry-changed', 00655 # self._entry_changed_callback) 00656 pass 00657 00658 def do_artists_load_finished(self, artists): 00659 self._load_model(iter(list(artists.values())), total=len(artists), progress=0.) 00660 self._album_manager.model.connect('album-added', self._on_album_added) 00661 00662 def _on_album_added(self, album_model, album): 00663 ''' 00664 called when album-manager album-added signal is invoked 00665 ''' 00666 print(album.artist) 00667 if self._artist_manager.model.contains(album.artist): 00668 print("contains artist") 00669 artist = self._artist_manager.model.get(album.artist) 00670 self._artist_manager.model.add_album_to_artist(artist, [album]) 00671 else: 00672 print("new artist") 00673 artist = Artist(album.artist, self._artist_manager.cover_man.unknown_cover) 00674 self._artist_manager.model.add(artist) 00675 00676 00677 class ArtistCoverManager(CoverManager): 00678 force_lastfm_check = True 00679 00680 def __init__(self, plugin, artist_manager): 00681 self.cover_db = CoverArtExtDB(name='artist-art') 00682 00683 super(ArtistCoverManager, self).__init__(plugin, artist_manager) 00684 00685 self.cover_size = 72 00686 00687 # create unknown cover and shadow for covers 00688 self.create_unknown_cover(plugin) 00689 00690 def create_unknown_cover(self, plugin): 00691 # create the unknown cover 00692 self.unknown_cover = self.create_cover( 00693 rb.find_plugin_file(plugin, 'img/microphone.png')) 00694 00695 super(ArtistCoverManager, self).create_unknown_cover(plugin) 00696 00697 def update_pixbuf_cover(self, coverobject, pixbuf): 00698 # if it's a pixbuf, assign it to all the artist for the artist 00699 key = RB.ExtDBKey.create_storage('artist', coverobject.name) 00700 00701 self.cover_db.store(key, RB.ExtDBSourceType.USER_EXPLICIT, 00702 pixbuf) 00703 00704 00705 class ArtistManager(GObject.Object): 00706 ''' 00707 Main construction that glues together the different managers, the loader 00708 and the model. It takes care of initializing all the system. 00709 00710 :param plugin: `Peas.PluginInfo` instance. 00711 :param current_view: `ArtistView` where the Artists are shown. 00712 ''' 00713 # singleton instance 00714 instance = None 00715 00716 # properties 00717 progress = GObject.property(type=float, default=0) 00718 00719 # signals 00720 __gsignals__ = { 00721 'sort': (GObject.SIGNAL_RUN_LAST, None, (object,)) 00722 } 00723 00724 def __init__(self, plugin, album_manager, shell): 00725 super(ArtistManager, self).__init__() 00726 00727 self.db = plugin.shell.props.db 00728 self.shell = shell 00729 self.plugin = plugin 00730 00731 self.cover_man = ArtistCoverManager(plugin, self) 00732 self.cover_man.album_manager = album_manager 00733 00734 self.model = ArtistsModel(album_manager) 00735 self.loader = ArtistLoader(self, album_manager) 00736 00737 # connect signals 00738 self._connect_signals() 00739 00740 def _connect_signals(self): 00741 ''' 00742 Connects the manager to all the needed signals for it to work. 00743 ''' 00744 self.loader.connect('model-load-finished', self._load_finished_callback) 00745 self.connect('sort', self._sort_artist) 00746 00747 def _sort_artist(self, widget, param): 00748 toolbar_type = param 00749 00750 if not toolbar_type or toolbar_type == "artist": 00751 self.model.sort() 00752 00753 def _load_finished_callback(self, *args): 00754 self.cover_man.load_covers() 00755 00756 00757 class ArtistShowingPolicy(GObject.Object): 00758 ''' 00759 Policy that mostly takes care of how and when things should be showed on 00760 the view that makes use of the `AlbumsModel`. 00761 ''' 00762 00763 def __init__(self, flow_view): 00764 super(ArtistShowingPolicy, self).__init__() 00765 00766 self._flow_view = flow_view 00767 self.counter = 0 00768 self._has_initialised = False 00769 00770 def initialise(self, album_manager): 00771 if self._has_initialised: 00772 return 00773 00774 self._has_initialised = True 00775 self._album_manager = album_manager 00776 self._model = album_manager.model 00777 00778 00779 class ArtistView(Gtk.TreeView, AbstractView): 00780 __gtype_name__ = "ArtistView" 00781 00782 name = 'artistview' 00783 icon_automatic = GObject.property(type=bool, default=True) 00784 panedposition = PanedCollapsible.Paned.COLLAPSE 00785 00786 __gsignals__ = { 00787 'update-toolbar': (GObject.SIGNAL_RUN_LAST, None, ()) 00788 } 00789 00790 00791 def __init__(self, *args, **kwargs): 00792 super(ArtistView, self).__init__(*args, **kwargs) 00793 00794 self._external_plugins = None 00795 self.gs = GSetting() 00796 self.show_policy = ArtistShowingPolicy(self) 00797 self.view = self 00798 self._has_initialised = False 00799 self._last_row_was_artist = False 00800 00801 def initialise(self, source): 00802 if self._has_initialised: 00803 return 00804 00805 self._has_initialised = True 00806 00807 self.view_name = "artist_view" 00808 super(ArtistView, self).initialise(source) 00809 self.album_manager = source.album_manager 00810 self.shell = source.shell 00811 self.props.has_tooltip = True 00812 00813 self.set_enable_tree_lines(True) 00814 00815 col = Gtk.TreeViewColumn(' ', Gtk.CellRendererText(), text=6) 00816 self.append_column(col) 00817 00818 pixbuf = Gtk.CellRendererPixbuf() 00819 col = Gtk.TreeViewColumn(_('Covers'), pixbuf, pixbuf=1) 00820 00821 self.append_column(col) 00822 00823 col = Gtk.TreeViewColumn(_('Artist'), Gtk.CellRendererText(), markup=5) 00824 self._artist_col = col 00825 col.set_clickable(True) 00826 col.set_sort_column_id(0) 00827 col.set_sort_indicator(True) 00828 col.connect('clicked', self._artist_sort_clicked) 00829 self.append_column(col) 00830 col = Gtk.TreeViewColumn('', Gtk.CellRendererText(), text=4) 00831 self.append_column(col) # dummy column to expand horizontally 00832 00833 self.artist_manager = self.album_manager.artist_man 00834 self.artist_manager.model.store.set_sort_column_id(0, Gtk.SortType.ASCENDING) 00835 self.set_model(self.artist_manager.model.store) 00836 00837 # setup iconview drag&drop support 00838 # first drag and drop on the coverart view to receive coverart 00839 self.enable_model_drag_dest([], Gdk.DragAction.COPY) 00840 self.drag_dest_add_image_targets() 00841 self.drag_dest_add_text_targets() 00842 self.connect('drag-drop', self.on_drag_drop) 00843 self.connect('drag-data-received', 00844 self.on_drag_data_received) 00845 00846 # lastly support drag-drop from coverart to devices/nautilus etc 00847 # n.b. enabling of drag-source is controlled by the selection-changed to ensure 00848 # we dont allow drag from artists 00849 self.connect('drag-begin', self.on_drag_begin) 00850 self._targets = Gtk.TargetList.new([Gtk.TargetEntry.new("text/uri-list", 0, 0)]) 00851 00852 # N.B. values taken from rhythmbox v2.97 widgets/rb_entry_view.c 00853 self._targets.add_uri_targets(1) 00854 self.connect("drag-data-get", self.on_drag_data_get) 00855 00856 # define artist specific popup menu 00857 self.artist_popup_menu = Menu(self.plugin, self.shell) 00858 self.artist_popup_menu.load_from_file('ui/coverart_artist_pop_rb2.ui', 00859 'ui/coverart_artist_pop_rb3.ui') 00860 signals = \ 00861 {'play_album_menu_item': self.source.play_album_menu_item_callback, 00862 'queue_album_menu_item': self.source.queue_album_menu_item_callback, 00863 'add_to_playing_menu_item': self.source.add_playlist_menu_item_callback, 00864 'new_playlist': self.source.add_playlist_menu_item_callback, 00865 'artist_cover_search_menu_item': self.cover_search_menu_item_callback 00866 } 00867 00868 self.artist_popup_menu.connect_signals(signals) 00869 self.artist_popup_menu.connect('pre-popup', self.pre_popup_menu_callback) 00870 00871 # connect properties and signals 00872 self._connect_properties() 00873 self._connect_signals() 00874 00875 def _connect_properties(self): 00876 setting = self.gs.get_setting(self.gs.Path.PLUGIN) 00877 setting.bind(self.gs.PluginKey.ICON_AUTOMATIC, self, 00878 'icon_automatic', Gio.SettingsBindFlags.GET) 00879 00880 def _connect_signals(self): 00881 self.connect('row-activated', self._row_activated) 00882 self.connect('row-expanded', self._row_expanded) 00883 self.connect('button-press-event', self._row_click) 00884 self.get_selection().connect('changed', self._selection_changed) 00885 self.connect('query-tooltip', self._query_tooltip) 00886 00887 def _artist_sort_clicked(self, *args): 00888 # in the absence of an apparent way to remove the unsorted default_sort_func 00889 # find out if we are now in an unsorted state - if we are 00890 # throw another clicked event so that we remain sorted. 00891 value, order = self.artist_manager.model.store.get_sort_column_id() 00892 00893 if order == None: 00894 self._artist_col.emit('clicked') 00895 00896 def cover_search_menu_item_callback(self, *args): 00897 self.artist_manager.cover_man.search_covers(self.get_selected_objects(just_artist=True), 00898 callback=self.source.update_request_status_bar) 00899 00900 def _query_tooltip(self, widget, x, y, key, tooltip): 00901 00902 try: 00903 winx, winy = self.convert_widget_to_bin_window_coords(x, y) 00904 treepath, treecolumn, cellx, celly = self.get_path_at_pos(winx, winy) 00905 active_object = self.artist_manager.model.get_from_path(treepath) 00906 00907 #active_object=self.artist_manager.model.store[treepath][self.artist_manager.model.columns['artist_album']] 00908 00909 if isinstance(active_object, Artist) and \ 00910 treecolumn.get_title() == _('Covers') and \ 00911 active_object.cover.original != self.artist_manager.cover_man.unknown_cover.original: 00912 # we display the tooltip if the row is an artist and the column 00913 # is actually the artist cover itself 00914 pixbuf = GdkPixbuf.Pixbuf.new_from_file(active_object.cover.original) 00915 00916 src_width = pixbuf.get_width() 00917 src_height = pixbuf.get_height() 00918 00919 factor = min(float(256) / float(src_width), float(256) / float(src_height)) 00920 new_width = int(src_width * factor + 0.5) 00921 new_height = int(src_height * factor + 0.5) 00922 00923 pixbuf = create_pixbuf_from_file_at_size( 00924 active_object.cover.original, new_width, new_height) 00925 00926 tooltip.set_icon(pixbuf) 00927 return True 00928 else: 00929 return False 00930 00931 except: 00932 pass 00933 00934 def _row_expanded(self, treeview, treeiter, treepath): 00935 ''' 00936 event called when clicking the expand icon on the treeview 00937 ''' 00938 self._row_activated(treeview, treepath, _) 00939 00940 def _row_activated(self, treeview, treepath, treeviewcolumn): 00941 ''' 00942 event called when double clicking on the tree-view or by keyboard ENTER 00943 ''' 00944 active_object = self.artist_manager.model.get_from_path(treepath) 00945 if isinstance(active_object, Artist): 00946 self.artist_manager.model.emit('update-path', treepath) 00947 else: 00948 #we need to play this album 00949 self.source.play_selected_album(self.source.favourites) 00950 00951 def pre_popup_menu_callback(self, *args): 00952 ''' 00953 callback when artist popup menu is about to be displayed 00954 ''' 00955 00956 state, sensitive = self.shell.props.shell_player.get_playing() 00957 if not state: 00958 sensitive = False 00959 00960 #self.popup_menu.get_menu_object('add_to_playing_menu_item') 00961 self.artist_popup_menu.set_sensitive('add_to_playing_menu_item', sensitive) 00962 00963 self.source.playlist_menu_item_callback() 00964 00965 def _row_click(self, widget, event): 00966 ''' 00967 event called when clicking on a row 00968 ''' 00969 print ('_row_click') 00970 00971 try: 00972 treepath, treecolumn, cellx, celly = self.get_path_at_pos(event.x, event.y) 00973 except: 00974 return 00975 00976 active_object = self.artist_manager.model.get_from_path(treepath) 00977 00978 if not isinstance(active_object, Album): 00979 self.source.artist_info.emit('selected', active_object.name, None) 00980 if self.icon_automatic: 00981 # reset counter so that we get correct double click action for albums 00982 self.source.click_count = 0 00983 00984 if treecolumn != self.get_expander_column(): 00985 if self.row_expanded(treepath) and event.button == 1 and self._last_row_was_artist: 00986 self.collapse_row(treepath) 00987 else: 00988 self.expand_row(treepath, False) 00989 00990 self._last_row_was_artist = True 00991 00992 if event.button == 3: 00993 # on right click 00994 # display popup 00995 00996 self.artist_popup_menu.popup(self.source, 'popup_menu', 3, 00997 Gtk.get_current_event_time()) 00998 print ('_row click artist exit') 00999 return 01000 01001 if event.button == 1: 01002 # on click 01003 # to expand the entry view 01004 ctrl = event.state & Gdk.ModifierType.CONTROL_MASK 01005 shift = event.state & Gdk.ModifierType.SHIFT_MASK 01006 01007 if self.icon_automatic and not self._last_row_was_artist: 01008 self.source.click_count += 1 if not ctrl and not shift else 0 01009 01010 if self.source.click_count == 1: 01011 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 250, 01012 self.source.show_hide_pane, active_object) 01013 01014 elif event.button == 3: 01015 # on right click 01016 # display popup 01017 01018 self.popup.popup(self.source, 'popup_menu', 3, 01019 Gtk.get_current_event_time()) 01020 01021 self._last_row_was_artist = False 01022 01023 print ('_row_click album exit') 01024 return 01025 01026 def get_view_icon_name(self): 01027 return "artistview.png" 01028 01029 def _selection_changed(self, *args): 01030 selected = self.get_selected_objects(just_artist=True) 01031 01032 print(selected) 01033 if len(selected) == 0: 01034 self.source.entry_view.clear() 01035 return 01036 01037 if isinstance(selected[0], Artist): 01038 print("selected artist") 01039 self.unset_rows_drag_source() # turn off drag-drop for artists 01040 01041 self.source.entryviewpane.update_cover(selected[0], 01042 self.artist_manager) 01043 else: 01044 print("selected album") 01045 self.source.update_with_selection() 01046 # now turnon drag-drop for album. 01047 self.enable_model_drag_source(Gdk.ModifierType.BUTTON1_MASK, 01048 [], Gdk.DragAction.COPY) 01049 self.drag_source_set_target_list(self._targets) 01050 01051 def switch_to_coverpane(self, cover_search_pane): 01052 ''' 01053 called from the source to update the coverpane when 01054 it is switched from the track pane 01055 This overrides the base method 01056 ''' 01057 01058 selected = self.get_selected_objects(just_artist=True) 01059 01060 if selected: 01061 manager = self.get_default_manager() 01062 self.source.entryviewpane.cover_search(selected[0], 01063 manager) 01064 01065 def get_selected_objects(self, just_artist=False): 01066 ''' 01067 finds what has been selected 01068 01069 returns an array of `Album` 01070 ''' 01071 selection = self.get_selection() 01072 model, treeiter = selection.get_selected() 01073 if treeiter: 01074 active_object = model.get_value(treeiter, ArtistsModel.columns['artist_album']) 01075 if isinstance(active_object, Album): 01076 # have chosen an album then just return that album 01077 return [active_object] 01078 else: 01079 # must have chosen an artist - return all albums for the artist by default 01080 # or just the artist itself 01081 if not just_artist: 01082 return self.artist_manager.model.get_albums(active_object.name) 01083 else: 01084 return [active_object] 01085 return [] 01086 01087 def switch_to_view(self, source, album): 01088 self.initialise(source) 01089 self.show_policy.initialise(source.album_manager) 01090 01091 self.scroll_to_album(album) 01092 01093 def scroll_to_album(self, album): 01094 if album: 01095 print("switch to artist view") 01096 print(album) 01097 artist = self.artist_manager.model.get(album.artist) 01098 path = self.artist_manager.model.get_path(artist) 01099 print(artist) 01100 print(path) 01101 path = self.artist_manager.model.store.convert_child_path_to_path(path) 01102 print(path) 01103 if path: 01104 self.scroll_to_cell(path, self._artist_col) 01105 self.expand_row(path, False) 01106 self.set_cursor(path) 01107 01108 def do_update_toolbar(self, *args): 01109 self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_BY) 01110 self.source.toolbar_manager.set_enabled(False, ToolbarObject.SORT_ORDER) 01111 self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_BY_ARTIST) 01112 self.source.toolbar_manager.set_enabled(True, ToolbarObject.SORT_ORDER_ARTIST) 01113 01114 def on_drag_drop(self, widget, context, x, y, time): 01115 ''' 01116 Callback called when a drag operation finishes over the view 01117 of the source. It decides if the dropped item can be processed as 01118 an image to use as a cover. 01119 ''' 01120 01121 # stop the propagation of the signal (deactivates superclass callback) 01122 widget.stop_emission_by_name('drag-drop') 01123 01124 # obtain the path of the icon over which the drag operation finished 01125 drop_info = self.get_dest_row_at_pos(x, y) 01126 path = None 01127 if drop_info: 01128 path, position = drop_info 01129 01130 result = path is not None 01131 01132 if result: 01133 target = self.drag_dest_find_target(context, None) 01134 widget.drag_get_data(context, target, time) 01135 01136 return result 01137 01138 def on_drag_data_received(self, widget, drag_context, x, y, data, info, 01139 time): 01140 ''' 01141 Callback called when the drag source has prepared the data (pixbuf) 01142 for us to use. 01143 ''' 01144 01145 # stop the propagation of the signal (deactivates superclass callback) 01146 widget.stop_emission_by_name('drag-data-received') 01147 01148 # get the artist and the info and ask the loader to update the cover 01149 path, position = self.get_dest_row_at_pos(x, y) 01150 artist_album = widget.get_model()[path][2] 01151 01152 pixbuf = data.get_pixbuf() 01153 01154 if isinstance(artist_album, Album): 01155 manager = self.album_manager 01156 else: 01157 manager = self.artist_manager 01158 01159 if pixbuf: 01160 manager.cover_man.update_cover(artist_album, pixbuf) 01161 else: 01162 uri = data.get_text() 01163 manager.cover_man.update_cover(artist_album, uri=uri) 01164 01165 # call the context drag_finished to inform the source about it 01166 drag_context.finish(True, False, time) 01167 01168 def on_drag_data_get(self, widget, drag_context, data, info, time): 01169 ''' 01170 Callback called when the drag destination (playlist) has 01171 requested what album (icon) has been dragged 01172 ''' 01173 01174 uris = [] 01175 for album in widget.get_selected_objects(): 01176 for track in album.get_tracks(): 01177 uris.append(track.location) 01178 01179 data.set_uris(uris) 01180 # stop the propagation of the signal (deactivates superclass callback) 01181 widget.stop_emission_by_name('drag-data-get') 01182 01183 def on_drag_begin(self, widget, context): 01184 ''' 01185 Callback called when the drag-drop from coverview has started 01186 Changes the drag icon as appropriate 01187 ''' 01188 album_number = len(widget.get_selected_objects()) 01189 01190 if album_number == 1: 01191 item = Gtk.STOCK_DND 01192 else: 01193 item = Gtk.STOCK_DND_MULTIPLE 01194 01195 widget.drag_source_set_icon_stock(item) 01196 widget.stop_emission_by_name('drag-begin') 01197 01198 def get_default_manager(self): 01199 ''' 01200 the default manager for this view is the artist_manager 01201 ''' 01202 return self.artist_manager