CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_artistview.py
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
 All Classes Functions