CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_album.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 '''
00021 Structures and managers to work with albums on Rhythmbox. This module provides
00022 the base model for the plugin to work on top of.
00023 '''
00024 
00025 from datetime import datetime, date
00026 import os
00027 import cgi
00028 import tempfile
00029 import gc
00030 
00031 from gi.repository import RB
00032 from gi.repository import GObject
00033 from gi.repository import Gio
00034 from gi.repository import GLib
00035 from gi.repository import Gtk
00036 from gi.repository import Gdk
00037 from gi.repository import GdkPixbuf
00038 import cairo
00039 
00040 from coverart_browser_prefs import GSetting
00041 from coverart_utils import create_pixbuf_from_file_at_size
00042 from coverart_utils import SortedCollection
00043 from coverart_utils import idle_iterator
00044 from coverart_utils import NaturalString
00045 import coverart_rb3compat as rb3compat
00046 from coverart_utils import uniquify_and_sort
00047 from coverart_utils import dumpstack
00048 from coverart_utils import check_lastfm
00049 import rb
00050 
00051 
00052 
00053 # default chunk of entries to process when loading albums
00054 ALBUM_LOAD_CHUNK = 50
00055 
00056 # default chunk of albums to process when loading covers
00057 COVER_LOAD_CHUNK = 5
00058 
00059 
00060 class Cover(GObject.Object):
00061     '''
00062     Cover of an Album. It may be initialized either by a file path to the image
00063     to use or by a previously allocated pixbuf.
00064 
00065     :param size: `int` size in pixels of the side of the cover (asuming a
00066         square-shapped cover).
00067     :param image: `str` containing a path of an image from where to create
00068         the cover.
00069     '''
00070     # signals
00071     __gsignals__ = {
00072         'resized': (GObject.SIGNAL_RUN_LAST, None, ())
00073     }
00074 
00075     def __init__(self, size, image):
00076         super(Cover, self).__init__()
00077 
00078         assert isinstance(image, str), "image should be a string"
00079 
00080         self.original = image
00081 
00082         self._create_pixbuf(size)
00083 
00084     def resize(self, size):
00085         '''
00086         Resizes the cover's pixbuf.
00087         '''
00088         if self.size != size:
00089             self._create_pixbuf(size)
00090             self.emit('resized')
00091 
00092     def _create_pixbuf(self, size):
00093         self.pixbuf = create_pixbuf_from_file_at_size(
00094             self.original, size, size)
00095 
00096         self.size = size
00097 
00098 
00099 class Shadow(Cover):
00100     SIZE = 120.
00101     WIDTH = 11
00102 
00103     def __init__(self, size, image):
00104         super(Shadow, self).__init__(size, image)
00105 
00106         self._calculate_sizes(size)
00107 
00108     def resize(self, size):
00109         super(Shadow, self).resize(size)
00110 
00111         self._calculate_sizes(size)
00112 
00113     def _calculate_sizes(self, size):
00114         self.width = int(size / self.SIZE * self.WIDTH)
00115         self.cover_size = self.size - self.width * 2
00116 
00117 
00118 class ShadowedCover(Cover):
00119     def __init__(self, shadow, image):
00120         super(ShadowedCover, self).__init__(shadow.cover_size, image)
00121 
00122         self._shadow = shadow
00123 
00124         self._add_shadow()
00125 
00126     def resize(self, size):
00127         if self.size != self._shadow.cover_size:
00128             self._create_pixbuf(self._shadow.cover_size)
00129             self._add_shadow()
00130 
00131             self.emit('resized')
00132 
00133     def _add_shadow(self):
00134         pix = self._shadow.pixbuf
00135 
00136         surface = cairo.ImageSurface(
00137             cairo.FORMAT_ARGB32, pix.get_width(), pix.get_height())
00138         context = cairo.Context(surface)
00139 
00140         # draw shadow
00141         Gdk.cairo_set_source_pixbuf(context, pix, 0, 0)
00142         context.paint()
00143 
00144         # draw cover
00145         Gdk.cairo_set_source_pixbuf(context, self.pixbuf, self._shadow.width,
00146                                     self._shadow.width)
00147         context.paint()
00148 
00149         self.pixbuf = Gdk.pixbuf_get_from_surface(surface, 0, 0,
00150                                                   self._shadow.size, self._shadow.size)
00151 
00152 
00153 class Track(GObject.Object):
00154     '''
00155     A music track. Provides methods to access to most of the tracks data from
00156     Rhythmbox's database.
00157 
00158     :param entry: `RB.RhythmbDBEntry` rhythmbox's database entry for the track.
00159     :param db: `RB.RhythmbDB` instance. It's needed to update the track's
00160         values.
00161     '''
00162     # signals
00163     __gsignals__ = {
00164         'modified': (GObject.SIGNAL_RUN_LAST, None, ()),
00165         'deleted': (GObject.SIGNAL_RUN_LAST, None, ())
00166     }
00167 
00168     __hash__ = GObject.__hash__
00169 
00170     def __init__(self, entry, db=None):
00171         super(Track, self).__init__()
00172 
00173         self.entry = entry
00174         self._db = db
00175 
00176     def __eq__(self, other):
00177         return rb.entry_equal(self.entry, other.entry)
00178 
00179     @property
00180     def title(self):
00181         return self.entry.get_string(RB.RhythmDBPropType.TITLE)
00182 
00183     @property
00184     def artist(self):
00185         return self.entry.get_string(RB.RhythmDBPropType.ARTIST)
00186 
00187     @property
00188     def album(self):
00189         return self.entry.get_string(RB.RhythmDBPropType.ALBUM)
00190 
00191     @property
00192     def album_artist(self):
00193         return self.entry.get_string(RB.RhythmDBPropType.ALBUM_ARTIST)
00194 
00195     @property
00196     def genre(self):
00197         return self.entry.get_string(RB.RhythmDBPropType.GENRE)
00198 
00199     @property
00200     def year(self):
00201         return self.entry.get_ulong(RB.RhythmDBPropType.DATE)
00202 
00203     @property
00204     def rating(self):
00205         return self.entry.get_double(RB.RhythmDBPropType.RATING)
00206 
00207     @rating.setter
00208     def rating(self, new_rating):
00209         self._db.entry_set(self.entry, RB.RhythmDBPropType.RATING, new_rating)
00210 
00211     @property
00212     def duration(self):
00213         return self.entry.get_ulong(RB.RhythmDBPropType.DURATION)
00214 
00215     @property
00216     def location(self):
00217         return self.entry.get_string(RB.RhythmDBPropType.LOCATION)
00218 
00219     @property
00220     def composer(self):
00221         return self.entry.get_string(RB.RhythmDBPropType.COMPOSER)
00222 
00223     @property
00224     def track_number(self):
00225         return self.entry.get_ulong(RB.RhythmDBPropType.TRACK_NUMBER)
00226 
00227     @property
00228     def disc_number(self):
00229         return self.entry.get_ulong(RB.RhythmDBPropType.DISC_NUMBER)
00230 
00231     @property
00232     def album_artist_sort(self):
00233         sort = self.entry.get_string(
00234             RB.RhythmDBPropType.ALBUM_ARTIST_SORTNAME_FOLDED) or \
00235                self.entry.get_string(RB.RhythmDBPropType.ALBUM_ARTIST_FOLDED) or \
00236                self.entry.get_string(RB.RhythmDBPropType.ARTIST_FOLDED)
00237 
00238         return NaturalString(sort)
00239 
00240     @property
00241     def album_sort(self):
00242         sort = self.entry.get_string(
00243             RB.RhythmDBPropType.ALBUM_SORTNAME_FOLDED) or \
00244                self.entry.get_string(RB.RhythmDBPropType.ALBUM_FOLDED)
00245 
00246         return NaturalString(sort)
00247 
00248     @property
00249     def is_saveable(self):
00250         return self.entry.get_entry_type().props.save_to_disk
00251 
00252     def create_ext_db_key(self):
00253         '''
00254         Returns an `RB.ExtDBKey` that can be used to acces/write some other
00255         track specific data on an `RB.ExtDB`.
00256         '''
00257         return self.entry.create_ext_db_key(RB.RhythmDBPropType.ALBUM)
00258 
00259 
00260 class Album(GObject.Object):
00261     '''
00262     An album. It's conformed from one or more tracks, and many of it's
00263     information is deduced from them.
00264 
00265     :param name: `str` name of the album.
00266     :param cover: `Cover` cover for this album.
00267     '''
00268     # signals
00269     __gsignals__ = {
00270         'modified': (GObject.SIGNAL_RUN_FIRST, None, ()),
00271         'emptied': (GObject.SIGNAL_RUN_LAST, None, ()),
00272         'cover-updated': (GObject.SIGNAL_RUN_LAST, None, ())
00273     }
00274 
00275     __hash__ = GObject.__hash__
00276 
00277     def __init__(self, name, artist, cover):
00278         super(Album, self).__init__()
00279 
00280         self.name = name
00281         self.artist = artist
00282         self._album_artist_sort = None
00283         self._album_sort = None
00284         self._artists = None
00285         self._titles = None
00286         self._composers = None
00287         self._genres = None
00288         self._tracks = []
00289         self._cover = None
00290         self.cover = cover
00291         self._year = None
00292         self._rating = None
00293         self._duration = None
00294 
00295         self._signals_id = {}
00296 
00297     @property
00298     def album_artist_sort(self):
00299         if not self._album_artist_sort:
00300             self._album_artist_sort = uniquify_and_sort(
00301                 [track.album_artist_sort for track in self._tracks])
00302 
00303         return self._album_artist_sort
00304 
00305     @property
00306     def album_sort(self):
00307         if not self._album_sort:
00308             self._album_sort = uniquify_and_sort(
00309                 [track.album_sort for track in self._tracks])
00310 
00311         return self._album_sort
00312 
00313     @property
00314     def artists(self):
00315         if not self._artists:
00316             self._artists = ', '.join(set(
00317                 [track.artist for track in self._tracks]))
00318 
00319         return self._artists
00320 
00321     @property
00322     def track_titles(self):
00323         if not self._titles:
00324             self._titles = ' '.join(set(
00325                 [track.title for track in self._tracks]))
00326 
00327         return self._titles
00328 
00329     @property
00330     def composers(self):
00331         if not self._composers:
00332             composers = [track.composer for track in self._tracks if track.composer]
00333             if composers:
00334                 self._composers = ' '.join(set(composers))
00335 
00336         return self._composers
00337 
00338     @property
00339     def year(self):
00340         if not self._year:
00341             real_years = [track.year for track in self._tracks if track.year != 0]
00342 
00343             if len(real_years) > 0:
00344                 self._year = min(real_years)
00345             else:
00346                 self._year = 0
00347 
00348         return self._year
00349 
00350     @property
00351     def real_year(self):
00352         ''' 
00353         return the calculated year e.g. 1989
00354         '''
00355         calc_year = self.year
00356 
00357         if calc_year == 0:
00358             calc_year = date.today().year
00359         else:
00360             calc_year = datetime.fromordinal(calc_year).year
00361 
00362         return calc_year
00363 
00364     @property
00365     def calc_year_sort(self):
00366         ''' 
00367         returns a str combinationi of real_year + album name
00368         '''
00369 
00370         return str(self.real_year) + self.name
00371 
00372     @property
00373     def genres(self):
00374         if not self._genres:
00375             self._genres = set([track.genre for track in self._tracks])
00376 
00377         return self._genres
00378 
00379     @property
00380     def rating(self):
00381         if not self._rating:
00382             ratings = [track.rating for track in self._tracks
00383                        if track.rating and track.rating != 0]
00384 
00385             if len(ratings) > 0:
00386                 self._rating = sum(ratings) / len(self._tracks)
00387             else:
00388                 self._rating = 0
00389         return self._rating
00390 
00391     @rating.setter
00392     def rating(self, new_rating):
00393         for track in self._tracks:
00394             track.rating = new_rating
00395         self._rating = None
00396         self.emit('modified')
00397 
00398     @property
00399     def track_count(self):
00400         return len(self._tracks)
00401 
00402     @property
00403     def duration(self):
00404         if not self._duration:
00405             self._duration = sum([track.duration for track in self._tracks])
00406 
00407         return self._duration
00408 
00409     @property
00410     def cover(self):
00411         return self._cover
00412 
00413     @cover.setter
00414     def cover(self, new_cover):
00415         if self._cover:
00416             self._cover.disconnect(self._cover_resized_id)
00417 
00418         self._cover = new_cover
00419         self._cover_resized_id = self._cover.connect('resized',
00420                                                      lambda *args: self.emit('cover-updated'))
00421 
00422         self.emit('cover-updated')
00423 
00424     def get_tracks(self, rating_threshold=0):
00425         '''
00426         Returns the tracks on this album. If rating_threshold is provided,
00427         only those tracks over the threshold will be returned. The track list
00428         returned is ordered by track number.
00429 
00430         :param rating_threshold: `float` threshold over which the rating of the
00431             track should be to be returned.
00432         '''
00433         if not rating_threshold:
00434             # if no threshold is set, return all
00435             tracks = self._tracks
00436         else:
00437             # otherwise, only return the entries over the threshold
00438             tracks = [track for track in self._tracks
00439                       if track.rating >= rating_threshold]
00440 
00441         return sorted(tracks, key=lambda track: (track.disc_number, track.track_number))
00442 
00443     def add_track(self, track):
00444         '''
00445         Adds a track to the album.
00446 
00447         :param track: `Track` track to be added.
00448         '''
00449         self._tracks.append(track)
00450         ids = (track.connect('modified', self._track_modified),
00451                track.connect('deleted', self._track_deleted))
00452 
00453         self._signals_id[track] = ids
00454         self.emit('modified')
00455 
00456     def _track_modified(self, track):
00457         print("_track_modified")
00458         if track.album != self.name:
00459             self._track_deleted(track)
00460         else:
00461             self.emit('modified')
00462 
00463     def _track_deleted(self, track):
00464         print("_track_deleted")
00465         self._tracks.remove(track)
00466 
00467         #list(map(track.disconnect, self._signals_id[track]))
00468         for signal_id in self._signals_id[track]:
00469             track.disconnect(signal_id)
00470 
00471         del self._signals_id[track]
00472 
00473         if len(self._tracks) == 0:
00474             self.emit('emptied')
00475         else:
00476             self.emit('modified')
00477 
00478     def create_ext_db_key(self):
00479         '''
00480         Creates a `RB.ExtDBKey` from this album's tracks.
00481         '''
00482         return self._tracks[0].create_ext_db_key()
00483 
00484     def do_modified(self):
00485         self._album_artist = None
00486         self._album_artist_sort = None
00487         self._album_sort = None
00488         self._artists = None
00489         self._titles = None
00490         self._genres = None
00491         self._year = None
00492         self._rating = None
00493         self._duration = None
00494         self._composers = None
00495 
00496     def __str__(self):
00497         return self.artist + self.name
00498 
00499     def __eq__(self, other):
00500         return other and self.name == other.name and \
00501                self.artist == other.artist
00502 
00503     def __ne__(self, other):
00504         return not other or \
00505                self.name + self.artist != other.name + other.artist
00506 
00507 
00508 class AlbumFilters(object):
00509     @classmethod
00510     def nay_filter(cls, *args):
00511         def filt(*args):
00512             return False
00513 
00514         return filt
00515 
00516     @classmethod
00517     def global_filter(cls, searchtext=None):
00518         def filt(album):
00519             # this filter is more complicated: for each word in the search
00520             # text, it tries to find at least one match on the params of
00521             # the album. If no match is given, then the album doesn't match
00522             if not searchtext:
00523                 return True
00524 
00525             words = RB.search_fold(searchtext).split()
00526             params = list(map(RB.search_fold, [album.name, album.artist,
00527                                                album.artists, album.track_titles, album.composers]))
00528             matches = []
00529 
00530             for word in words:
00531                 match = False
00532 
00533                 for param in params:
00534                     if word in param:
00535                         match = True
00536                         break
00537 
00538                 matches.append(match)
00539 
00540             return False not in matches
00541 
00542         return filt
00543 
00544     @classmethod
00545     def album_artist_filter(cls, searchtext=None):
00546         def filt(album):
00547             if not searchtext:
00548                 return True
00549 
00550             return RB.search_fold(searchtext) in RB.search_fold(album.artist)
00551 
00552         return filt
00553 
00554     @classmethod
00555     def artist_filter(cls, searchtext=None):
00556         def filt(album):
00557             if not searchtext:
00558                 return True
00559 
00560             return RB.search_fold(searchtext) in RB.search_fold(album.artists)
00561 
00562         return filt
00563 
00564     @classmethod
00565     def similar_artist_filter(cls, searchtext=None):
00566         def filt(album):
00567             # this filter is more complicated: for each word in the search
00568             # text, it tries to find at least one match on the params of
00569             # the album. If no match is given, then the album doesn't match
00570             if not searchtext:
00571                 return True
00572 
00573             words = RB.search_fold(searchtext).split()
00574             params = list(map(RB.search_fold, [album.artist,
00575                                                album.artists]))
00576             matches = []
00577 
00578             for word in words:
00579                 match = False
00580 
00581                 for param in params:
00582                     if word in param:
00583                         match = True
00584                         break
00585 
00586                 matches.append(match)
00587 
00588             return False not in matches
00589 
00590         return filt
00591 
00592     @classmethod
00593     def album_name_filter(cls, searchtext=None):
00594         def filt(album):
00595             if not searchtext:
00596                 return True
00597 
00598             return RB.search_fold(searchtext) in RB.search_fold(album.name)
00599 
00600         return filt
00601 
00602     @classmethod
00603     def track_title_filter(cls, searchtext=None):
00604         def filt(album):
00605             if not searchtext:
00606                 return True
00607 
00608             return RB.search_fold(searchtext) in RB.search_fold(
00609                 album.track_titles)
00610 
00611         return filt
00612 
00613     @classmethod
00614     def composer_filter(cls, searchtext=None):
00615         def filt(album):
00616             if not searchtext:
00617                 return True
00618 
00619             return RB.search_fold(searchtext) in RB.search_fold(
00620                 album.composers)
00621 
00622         return filt
00623 
00624     @classmethod
00625     def genre_filter(cls, searchtext=None):
00626         def filt(album):
00627             if not searchtext:
00628                 return True
00629 
00630             genres = RB.search_fold(' '.join(album.genres))
00631             return RB.search_fold(searchtext) in genres
00632 
00633         return filt
00634 
00635     @classmethod
00636     def model_filter(cls, model=None):
00637         if not model or not len(model):
00638             return lambda x: False
00639 
00640         albums = set()
00641 
00642         for row in model:
00643             entry = model[row.path][0]
00644             albums.add(Track(entry).album)
00645 
00646         def filt(album):
00647             return album.name in albums
00648 
00649         return filt
00650 
00651     @classmethod
00652     def decade_filter(cls, searchdecade=None):
00653         '''
00654         The year is in RATA DIE format so need to extract the year
00655 
00656         The searchdecade param can be None meaning all results
00657         or -1 for all albums older than our standard range which is 1930
00658         or an actual decade for 1930 to 2020
00659         '''
00660 
00661         def filt(album):
00662             if not searchdecade:
00663                 return True
00664 
00665             if album.year == 0:
00666                 year = date.today().year
00667             else:
00668                 year = datetime.fromordinal(album.year).year
00669 
00670             year = int(round(year - 5, -1))
00671 
00672             if searchdecade > 0:
00673                 return searchdecade == year
00674             else:
00675                 return year < 1930
00676 
00677         return filt
00678 
00679 
00680 AlbumFilters.keys = {
00681     'nay': AlbumFilters.nay_filter,
00682     'all': AlbumFilters.global_filter,
00683     'album_artist': AlbumFilters.album_artist_filter,
00684     'artist': AlbumFilters.artist_filter,
00685     'quick_artist': AlbumFilters.artist_filter,
00686     'composers': AlbumFilters.composer_filter,
00687     'similar_artist': AlbumFilters.similar_artist_filter,
00688     'album_name': AlbumFilters.album_name_filter,
00689     'track': AlbumFilters.track_title_filter,
00690     'genre': AlbumFilters.genre_filter,
00691     'model': AlbumFilters.model_filter,
00692     'decade': AlbumFilters.decade_filter
00693 }
00694 
00695 sort_keys = {
00696     'name': ('album_sort', 'album_sort'),
00697     'artist': ('album_artist_sort', 'album_artist_sort'),
00698     'year': ('year', 'album_sort'),
00699     'rating': ('rating', 'album_sort'),
00700 }
00701 
00702 
00703 class AlbumsModel(GObject.Object):
00704     '''
00705     Model that contains albums, keeps them sorted, filtered and provides an
00706     external `Gtk.TreeModel` interface to use as part of a Gtk interface.
00707 
00708     The `Gtk.TreeModel` haves the following structure:
00709     column 0 -> string containing the album name and artist
00710     column 1 -> pixbuf of the album's cover.
00711     column 2 -> instance of the album itself.
00712     column 3 -> markup text showed under the cover.
00713     column 4 -> boolean that indicates if the row should be shown
00714     '''
00715     # signals
00716     __gsignals__ = {
00717         'generate-tooltip': (GObject.SIGNAL_RUN_LAST, str, (object,)),
00718         'generate-markup': (GObject.SIGNAL_RUN_LAST, str, (object,)),
00719         'album-updated': ((GObject.SIGNAL_RUN_LAST, None, (object, object))),
00720         'visual-updated': ((GObject.SIGNAL_RUN_LAST, None, (object, object))),
00721         'filter-changed': ((GObject.SIGNAL_RUN_FIRST, None, ())),
00722         'album-added': ((GObject.SIGNAL_RUN_LAST, None, (object,)))
00723     }
00724 
00725     # list of columns names and positions on the TreeModel
00726     columns = {'tooltip': 0, 'pixbuf': 1, 'album': 2, 'markup': 3, 'show': 4}
00727 
00728     def __init__(self):
00729         super(AlbumsModel, self).__init__()
00730 
00731         self._iters = {}
00732         self._albums = SortedCollection(
00733             key=lambda album: getattr(album, 'name'))
00734         self._sortkey = {'type': 'name', 'order': True}
00735 
00736         self._tree_store = Gtk.ListStore(str, GdkPixbuf.Pixbuf, object, str,
00737                                          bool)
00738 
00739         # filters
00740         self._filters = {}
00741 
00742         # sorting idle call
00743         self._sort_process = None
00744 
00745         # create the filtered store that's used with the view
00746         self._filtered_store = self._tree_store.filter_new()
00747         self._filtered_store.set_visible_column(AlbumsModel.columns['show'])
00748 
00749     @property
00750     def store(self):
00751         return self._filtered_store
00752 
00753     @idle_iterator
00754     def _recreate_text(self):
00755         def process(album, data):
00756             tree_iter = self._iters[album.name][album.artist]['iter']
00757             markup = self.emit('generate-markup', album)
00758 
00759             self._tree_store.set(tree_iter, self.columns['markup'],
00760                                  markup)
00761             self._emit_signal(tree_iter, 'visual-updated')
00762 
00763         def error(exception):
00764             print('Error while recreating text: ' + str(exception))
00765 
00766         return ALBUM_LOAD_CHUNK, process, None, error, None
00767 
00768     def _album_modified(self, album):
00769         print("_album_modified")
00770         tree_iter = self._iters[album.name][album.artist]['iter']
00771 
00772         if self._tree_store.iter_is_valid(tree_iter):
00773             # only update if the iter is valid
00774             # generate and update values
00775             tooltip, pixbuf, album, markup, hidden = \
00776                 self._generate_values(album)
00777 
00778             self._tree_store.set(tree_iter, self.columns['tooltip'], tooltip,
00779                                  self.columns['markup'], markup, self.columns['show'], hidden)
00780 
00781             # reorder the album
00782             new_pos = self._albums.reorder(album)
00783 
00784             if new_pos != -1:
00785                 if (new_pos + 1) >= len(self._albums):
00786                     old_album = self._albums[new_pos - 1]
00787                     old_iter = \
00788                         self._iters[old_album.name][old_album.artist]['iter']
00789                     self._tree_store.move_after(tree_iter, old_iter)
00790                 else:
00791                     old_album = self._albums[new_pos + 1]
00792                     old_iter = \
00793                         self._iters[old_album.name][old_album.artist]['iter']
00794                     self._tree_store.move_before(tree_iter, old_iter)
00795 
00796             # inform that the album is updated
00797             print("album modified")
00798             print(album)
00799             self._emit_signal(tree_iter, 'album-updated')
00800 
00801     def _cover_updated(self, album):
00802         tree_iter = self._iters[album.name][album.artist]['iter']
00803 
00804         if self._tree_store.iter_is_valid(tree_iter):
00805             # only update if the iter is valid
00806             pixbuf = album.cover.pixbuf
00807 
00808             self._tree_store.set_value(tree_iter, self.columns['pixbuf'],
00809                                        pixbuf)
00810 
00811             self._emit_signal(tree_iter, 'visual-updated')
00812 
00813     def _emit_signal(self, tree_iter, signal):
00814         # we get the filtered path and iter since that's what the outside world
00815         # interacts with
00816         tree_path = self._filtered_store.convert_child_path_to_path(
00817             self._tree_store.get_path(tree_iter))
00818 
00819         if tree_path:
00820             # if there's no path, the album doesn't show on the filtered model
00821             # so no one needs to know
00822             tree_iter = self._filtered_store.get_iter(tree_path)
00823 
00824             self.emit(signal, tree_path, tree_iter)
00825 
00826     def add(self, album):
00827         '''
00828         Add an album to the model.
00829 
00830         :param album: `Album` to be added to the model.
00831         '''
00832 
00833         # generate necessary values
00834         values = self._generate_values(album)
00835         # insert the values
00836         tree_iter = self._tree_store.insert(self._albums.insert(album), values)
00837         # connect signals
00838         ids = (album.connect('modified', self._album_modified),
00839                album.connect('cover-updated', self._cover_updated),
00840                album.connect('emptied', self.remove))
00841         if not album.name in self._iters:
00842             self._iters[album.name] = {}
00843         self._iters[album.name][album.artist] = {'album': album,
00844                                                  'iter': tree_iter, 'ids': ids}
00845         self.emit('album-added', album)
00846         return tree_iter
00847 
00848     def _generate_values(self, album):
00849         tooltip = self.emit('generate-tooltip', album)
00850         markup = self.emit('generate-markup', album)
00851         pixbuf = album.cover.pixbuf
00852         hidden = self._album_filter(album)
00853 
00854         return tooltip, pixbuf, album, markup, hidden
00855 
00856     def remove(self, album):
00857         '''
00858         Removes this album from the model.
00859 
00860         :param album: `Album` to be removed from the model.
00861         '''
00862         print("album model remove")
00863         print(album)
00864         self._albums.remove(album)
00865         self._tree_store.remove(self._iters[album.name][album.artist]['iter'])
00866 
00867         # disconnect signals
00868         for sig_id in self._iters[album.name][album.artist]['ids']:
00869             album.disconnect(sig_id)
00870 
00871         del self._iters[album.name][album.artist]
00872 
00873     def contains(self, album_name, album_artist):
00874         '''
00875         Indicates if the model contains a specific album.
00876 
00877         :param album_name: `str` name of the album.
00878         '''
00879         return album_name in self._iters \
00880             and album_artist in self._iters[album_name]
00881 
00882     def get(self, album_name, album_artist):
00883         '''
00884         Returns the requested album.
00885 
00886         :param album_name: `str` name of the album.
00887         '''
00888         return self._iters[album_name][album_artist]['album']
00889 
00890     def get_from_dbentry(self, entry):
00891         '''
00892         Returns the album containing the track corresponding to rhythmdbentry
00893         
00894         :param entry: `RhythmDBEntry`
00895         '''
00896 
00897         album_artist = entry.get_string(RB.RhythmDBPropType.ALBUM_ARTIST)
00898         album_artist = album_artist if album_artist else entry.get_string(RB.RhythmDBPropType.ARTIST)
00899         album_name = entry.get_string(RB.RhythmDBPropType.ALBUM)
00900 
00901         return self._iters[album_name][album_artist]['album']
00902 
00903     def get_all(self):
00904         '''
00905         Returns a collection of all the albums in this model.
00906         '''
00907         return self._albums
00908 
00909     def get_from_path(self, path):
00910         '''
00911         Returns an album referenced by a `Gtk.TreeModel` path.
00912 
00913         :param path: `Gtk.TreePath` referencing the album.
00914         '''
00915         return self._filtered_store[path][self.columns['album']]
00916 
00917     def get_from_ext_db_key(self, key):
00918         '''
00919         Returns the requested album.
00920 
00921         :param key: ext_db_key
00922         '''
00923         # get the album name and artist
00924         name = key.get_field('album')
00925         artist = key.get_field('artist')
00926 
00927         # first check if there's a direct match
00928         album = self.get(name, artist) if self.contains(name, artist) else None
00929 
00930         if not album:
00931             # get all the albums with the given name and look for a match
00932             albums = [artist['album'] for artist in list(self._iters[name].values())]
00933 
00934             for curr_album in albums:
00935                 if key.matches(curr_album.create_ext_db_key()):
00936                     album = curr_album
00937                     break
00938 
00939         return album
00940 
00941     def get_path(self, album):
00942         return self._filtered_store.convert_child_path_to_path(
00943             self._tree_store.get_path(
00944                 self._iters[album.name][album.artist]['iter']))
00945 
00946     def find_first_visible(self, filter_key, filter_arg, start=None,
00947                            backwards=False):
00948         album_filter = AlbumFilters.keys[filter_key](filter_arg)
00949 
00950         albums = reversed(self._albums) if backwards else self._albums
00951         ini = albums.index(start) + 1 if start else 0
00952 
00953         for i in range(ini, len(albums)):
00954             album = albums[i]
00955 
00956             if album_filter(album) and self._album_filter(album):
00957                 return album
00958 
00959         return None
00960 
00961     def show(self, album, show):
00962         '''
00963         Unfilters an album, making it visible to the publicly available model's
00964         `Gtk.TreeModel`
00965 
00966         :param album: `Album` to show or hide.
00967         :param show: `bool` indcating whether to show(True) or hide(False) the
00968             album.
00969         '''
00970         album_iter = self._iters[album.name][album.artist]['iter']
00971 
00972         if self._tree_store.iter_is_valid(album_iter):
00973             self._tree_store.set_value(album_iter, self.columns['show'], show)
00974 
00975     @idle_iterator
00976     def _sort(self):
00977         def process(album, data):
00978             values = self._generate_values(album)
00979 
00980             tree_iter = self._tree_store.append(values)
00981             self._iters[album.name][album.artist]['iter'] = tree_iter
00982 
00983         def error(exception):
00984             print('Error(1) while adding albums to the model: ' + str(exception))
00985 
00986         def finish(data):
00987             self._sort_process = None
00988             self.remove_filter('nay')
00989 
00990         return ALBUM_LOAD_CHUNK, process, None, error, finish
00991 
00992     def sort(self):
00993         '''
00994         Changes the sorting strategy for the model.
00995         '''
00996 
00997         gs = GSetting()
00998         source_settings = gs.get_setting(gs.Path.PLUGIN)
00999         key = source_settings[gs.PluginKey.SORT_BY]
01000         order = source_settings[gs.PluginKey.SORT_ORDER]
01001 
01002         print("current")
01003         print(self._sortkey)
01004 
01005         print("registry")
01006         print(key)
01007         print(order)
01008 
01009         if key == self._sortkey['type']:
01010             key = None
01011         else:
01012             self._sortkey['type'] = key
01013 
01014         if order != self._sortkey['order']:
01015             reverse = True
01016             self._sortkey['order'] = order
01017         else:
01018             reverse = False
01019 
01020         def key_function(album):
01021             keys = [getattr(album, prop) for prop in props]
01022             return keys
01023 
01024         if not key and not reverse:
01025             print("nothing to sort")
01026             return
01027 
01028         print(key)
01029         print(reverse)
01030         if key:
01031             props = sort_keys[key]
01032             self._albums.key = key_function
01033 
01034         if reverse:
01035             self._albums = reversed(self._albums)
01036 
01037         self._tree_store.clear()
01038 
01039         # add the nay filter
01040         self.replace_filter('nay', refilter=False)
01041 
01042         if self._sort_process:
01043             # stop the previous sort process if there's one
01044             self._sort_process.stop()
01045 
01046         # load the albums back to the model
01047         self._sort_process = self._sort(iter(self._albums))
01048 
01049     def replace_filter(self, filter_key, filter_arg=None, refilter=True):
01050         '''
01051         Adds or replaces a filter by it's filter_key.
01052 
01053         :param filter_key: `str` key of the filter method to use. This should
01054             be one of the available keys on the `AlbumFilters` class.
01055         :param filter_arg: `object` any object that the correspondant filter
01056             method may need to perform the filtering process.
01057         :param refilter: `bool` indicating whether to force a refilter and
01058         emit the 'filter-changed' signal(True) or not(False).
01059         '''
01060         self._filters[filter_key] = AlbumFilters.keys[filter_key](filter_arg)
01061 
01062         if refilter:
01063             self.emit('filter-changed')
01064 
01065     def remove_filter(self, filter_key, refilter=True):
01066         '''
01067         Removes a filter by it's filter_key
01068 
01069         :param filter_key: `str` key of the filter method to use. This should
01070             be one of the available keys on the `AlbumFilters` class.
01071         :param refilter: `bool` indicating whether to force a refilter and
01072         emit the 'filter-changed' signal(True) or not(False).
01073         '''
01074         if filter_key in self._filters:
01075             del self._filters[filter_key]
01076 
01077             if refilter:
01078                 self.emit('filter-changed')
01079 
01080     def clear_filters(self):
01081         '''
01082         Clears all filters on the model.
01083         '''
01084         if self._filters:
01085             self._filters.clear()
01086 
01087             self.emit('filter-changed')
01088 
01089     def do_filter_changed(self):
01090         pos = 0
01091         for show_result in list(map(self._album_filter, self._albums)):
01092             self.show(self._albums[pos], show_result)
01093             pos = pos + 1
01094 
01095     def _album_filter(self, album):
01096         for f in list(self._filters.values()):
01097             if not f(album):
01098                 return False
01099 
01100         return True
01101 
01102     def recreate_text(self):
01103         '''
01104         Forces the recreation and update of the markup text for each album.
01105         '''
01106         self._recreate_text(iter(self._albums))
01107 
01108 
01109 class AlbumLoader(GObject.Object):
01110     '''
01111     Loads and updates Rhythmbox's tracks and albums, updating the model
01112     accordingly.
01113 
01114     :param album_manager: `AlbumManager` responsible for this loader.
01115     '''
01116     # signals
01117     __gsignals__ = {
01118         'albums-load-finished': (GObject.SIGNAL_RUN_LAST, None, (object,)),
01119         'model-load-finished': (GObject.SIGNAL_RUN_LAST, None, ())
01120     }
01121 
01122     def __init__(self, album_manager):
01123         super(AlbumLoader, self).__init__()
01124 
01125         self._album_manager = album_manager
01126         self._tracks = {}
01127 
01128         self._connect_signals()
01129 
01130     def _connect_signals(self):
01131         # connect signals for updating the albums
01132         self.entry_changed_id = self._album_manager.db.connect('entry-changed',
01133                                                                self._entry_changed_callback)
01134         self.entry_added_id = self._album_manager.db.connect('entry-added',
01135                                                              self._entry_added_callback)
01136         self.entry_deleted_id = self._album_manager.db.connect('entry-deleted',
01137                                                                self._entry_deleted_callback)
01138 
01139     @idle_iterator
01140     def _load_albums(self):
01141         def process(row, data):
01142             entry = data['model'][row.path][0]
01143 
01144             # allocate the track
01145             track = Track(entry, self._album_manager.db)
01146             self._tracks[track.location] = track
01147 
01148             album_name = track.album
01149             album_artist = track.album_artist
01150             album_artist = album_artist if album_artist else track.artist
01151 
01152             if album_name not in data['albums']:
01153                 data['albums'][album_name] = {}
01154 
01155             if album_artist in data['albums'][album_name]:
01156                 album = data['albums'][album_name][album_artist]
01157             else:
01158                 album = Album(album_name, album_artist,
01159                               self._album_manager.cover_man.unknown_cover)
01160                 data['albums'][album_name][album_artist] = album
01161 
01162             album.add_track(track)
01163 
01164         def after(data):
01165             # update the progress
01166             data['progress'] += ALBUM_LOAD_CHUNK
01167 
01168             self._album_manager.progress = data['progress'] / data['total']
01169 
01170         def error(exception):
01171             print('Error processing entries: ' + str(exception))
01172 
01173         def finish(data):
01174             self._album_manager.progress = 1
01175             self.emit('albums-load-finished', data['albums'])
01176 
01177         return ALBUM_LOAD_CHUNK, process, after, error, finish
01178 
01179     @idle_iterator
01180     def _load_model(self):
01181         def process(albums, data):
01182             # add  the album to the model
01183             for album in list(albums.values()):
01184                 self._album_manager.model.add(album)
01185 
01186         def after(data):
01187             data['progress'] += ALBUM_LOAD_CHUNK
01188 
01189             # update the progress
01190             self._album_manager.progress = 1 - data['progress'] / data['total']
01191 
01192         def error(exception):
01193             dumpstack("Something awful happened!")
01194             print('Error(2) while adding albums to the model: ' + str(exception))
01195 
01196         def finish(data):
01197             self._album_manager.progress = 0
01198             self.emit('model-load-finished')
01199             return False
01200 
01201         return ALBUM_LOAD_CHUNK, process, after, error, finish
01202 
01203     def _entry_changed_callback(self, db, entry, changes):
01204         print("CoverArtBrowser DEBUG - entry_changed_callback")
01205         # NOTE: changes are packed in array of rhythmdbentrychange
01206 
01207         def analyse_change(change):
01208             print(change.prop)
01209             if change.prop is RB.RhythmDBPropType.ALBUM \
01210                     or change.prop is RB.RhythmDBPropType.ALBUM_ARTIST \
01211                     or change.prop is RB.RhythmDBPropType.ARTIST \
01212                     or change.prop is RB.RhythmDBPropType.ALBUM_SORTNAME \
01213                     or change.prop is RB.RhythmDBPropType.ALBUM_ARTIST_SORTNAME:
01214                 # called when the album of a entry is modified
01215                 track.emit('deleted')
01216                 track.emit('modified')
01217                 print("change prop album or artist")
01218                 self._allocate_track(track)
01219 
01220             elif change.prop is RB.RhythmDBPropType.HIDDEN:
01221                 # called when an entry gets hidden (e.g.:the sound file is
01222                 # removed.
01223                 if changes.new:
01224                     print("change prop new")
01225                     track.emit('deleted')
01226                 else:
01227                     print("change prop dunno")
01228                     self._allocate_track(track)
01229 
01230         # look at all the changes and update the albums accordingly
01231         track = self._tracks[Track(entry).location]
01232 
01233         #RB3 has a simple rhythmdbentrychange array to deal with so we
01234         #just need to loop each element of the array
01235 
01236         for change in changes:
01237             analyse_change(change)
01238 
01239         print("CoverArtBrowser DEBUG - end entry_changed_callback")
01240 
01241     def _entry_added_callback(self, db, entry):
01242         print("CoverArtBrowser DEBUG - entry_added_callback")
01243         self._allocate_track(Track(entry, db))
01244 
01245         print("CoverArtBrowser DEBUG - end entry_added_callback")
01246 
01247     def _entry_deleted_callback(self, db, entry):
01248         print("CoverArtBrowser DEBUG - entry_deleted_callback")
01249         prototype = Track(entry).location
01250 
01251         if prototype in self._tracks:
01252             # gotta check if the track is loaded first
01253             track = self._tracks[prototype]
01254             del self._tracks[track.location]
01255 
01256             track.emit('deleted')
01257 
01258         print("CoverArtBrowser DEBUG - end entry_deleted_callback")
01259 
01260     def _allocate_track(self, track):
01261         if track.duration > 0 and track.is_saveable:
01262             # only allocate the track if it's a valid track
01263             self._tracks[track.location] = track
01264 
01265             album_name = track.album
01266             album_artist = track.album_artist
01267             album_artist = album_artist if album_artist else track.artist
01268 
01269             if self._album_manager.model.contains(album_name, album_artist):
01270                 print("allocate track - contains")
01271                 album = self._album_manager.model.get(album_name, album_artist)
01272                 print(album)
01273                 album.add_track(track)
01274             else:
01275                 print("allocate track - does not contain")
01276                 album = Album(album_name, album_artist,
01277                               self._album_manager.cover_man.unknown_cover)
01278                 print(album)
01279                 album.add_track(track)
01280                 self._album_manager.cover_man.load_cover(album)
01281                 self._album_manager.model.add(album)
01282 
01283     def load_albums(self, query_model):
01284         '''
01285         Loads and creates `Track` instances for all entries on query_model,
01286         assigning them into their correspondant `Album`.
01287         '''
01288         print("CoverArtBrowser DEBUG - load_albums")
01289 
01290         self._load_albums(iter(query_model), albums={}, model=query_model,
01291                           total=len(query_model), progress=0.)
01292 
01293         print("CoverArtBrowser DEBUG - load_albums finished")
01294 
01295     def do_albums_load_finished(self, albums):
01296         # load the albums to the model
01297         self._album_manager.model.replace_filter('nay')
01298         self._load_model(iter(list(albums.values())), total=len(albums), progress=0.)
01299 
01300     def do_model_load_finished(self):
01301         self._album_manager.model.remove_filter('nay')
01302 
01303 
01304 class CoverRequester(GObject.Object):
01305     def __init__(self, cover_db):
01306         super(CoverRequester, self).__init__()
01307 
01308         self._cover_db = cover_db
01309         self.unknown_cover = None
01310         self._callback = None
01311         self._queue = []
01312         self._queue_id = 0
01313         self._running = False
01314         self._stop = False
01315 
01316     def add_to_queue(self, coverobjects, callback):
01317         ''' Adds coverobjects to the queue if they're not already there. '''
01318         self._queue.extend(
01319             [coverobject for coverobject in coverobjects if coverobject not in self._queue])
01320 
01321         self._start_process(callback)
01322 
01323     def replace_queue(self, coverobjects, callback):
01324         ''' Completely replace the current queue. '''
01325         self._queue = coverobjects
01326 
01327         self._start_process(callback)
01328 
01329     def _start_process(self, callback):
01330         ''' Starts the queue processing if it isn't running already '''
01331         if not self._running:
01332             self._callback = callback
01333             self._running = True
01334             self._process_queue()
01335 
01336     def _process_queue(self):
01337         '''
01338         Main method that process the queue.
01339         First, it tries to adquire a lock on the queue, and if it can, pops
01340         the next element of the queue and process it.
01341         The lock makes sure that only one request is done at a time, and
01342         successfully ignores false timeouts or strand callbacks.
01343         '''
01344         # process the next element in the queue
01345         while self._queue:
01346             coverobject = self._queue.pop(0)
01347 
01348             if coverobject.cover is self.unknown_cover:
01349                 break
01350         else:
01351             coverobject = None
01352 
01353         if coverobject:
01354             # inform the current coverobject being searched
01355             self._callback(coverobject)
01356 
01357             # start the request
01358             self._queue_id += 1
01359             self._search_for_cover(coverobject, self._queue_id)
01360 
01361             # add a timeout to the request
01362             Gdk.threads_add_timeout_seconds(GLib.PRIORITY_DEFAULT_IDLE, 40,
01363                                             self._next, self._queue_id)
01364         else:
01365             # if there're no more elements, clean the state of the requester
01366             self._running = False
01367             self._callback(None)
01368 
01369     def _search_for_cover(self, coverobject, search_id):
01370         '''
01371         Activelly requests a cover to the cover_db, calling
01372         the callback given once the process finishes (since it generally is
01373         asynchronous).
01374         For more information on the callback arguments, check
01375         `RB.ExtDB.request` documentation.
01376 
01377         :param coverobject: covertype for which search the cover.
01378         '''
01379         # create a key and request the cover
01380         key = coverobject.create_ext_db_key()
01381         provides = self._cover_db.request(key, self._next, search_id)
01382 
01383         if not provides:
01384             # in case there is no provider, call the callback immediately
01385             self._next(search_id)
01386 
01387     def _next(self, *args):
01388         ''' Advances to the next coverobject to process. '''
01389         # get the id of the search
01390         search_id = args[-1]
01391         if search_id == self._queue_id:
01392             # only process the next element if the search_id is the same as
01393             # the current id. Otherwise, this is a invalid call
01394             self._process_queue()
01395 
01396     def stop(self):
01397         ''' Clears the queue, forcing the requester to stop. '''
01398         del self._queue[:]
01399 
01400 
01401 class CoverManager(GObject.Object):
01402     '''
01403     Manager that takes care of cover loading and updating.
01404 
01405     :param plugin: `Peas.PluginInfo` instance used to have access to the
01406         predefined unknown cover.
01407     :param album_manager: `AlbumManager` responsible for this manager.
01408     '''
01409 
01410     # signals
01411     __gsignals__ = {
01412         'load-finished': (GObject.SIGNAL_RUN_LAST, None, ())
01413     }
01414 
01415     # properties
01416     has_finished_loading = False
01417     force_lastfm_check = False
01418     cover_size = GObject.property(type=int, default=0)
01419 
01420     def __init__(self, plugin, manager):
01421         super(CoverManager, self).__init__()
01422         #self.cover_db = None to be defined by inherited class
01423         self._manager = manager
01424         self._requester = CoverRequester(self.cover_db)
01425 
01426         self.unknown_cover = None  #to be defined by inherited class
01427         self.album_manager = None  #to be defined by inherited class
01428 
01429         # connect the signal to update cover arts when added
01430         self.req_id = self.cover_db.connect('added',
01431                                             self.coverart_added_callback)
01432         self.connect('load-finished', self._on_load_finished)
01433 
01434     def _on_load_finished(self, *args):
01435         self.has_finished_loading = True
01436 
01437     @idle_iterator
01438     def _load_covers(self):
01439         def process(coverobject, data):
01440             self.load_cover(coverobject)
01441 
01442         def finish(data):
01443             self.album_manager.progress = 1
01444             gc.collect()
01445             self.emit('load-finished')
01446 
01447         def error(exception):
01448             print('Error while loading covers: ' + str(exception))
01449 
01450         def after(data):
01451             data['progress'] += COVER_LOAD_CHUNK
01452 
01453             # update the progress
01454             self.album_manager.progress = data['progress'] / data['total']
01455 
01456         return COVER_LOAD_CHUNK, process, after, error, finish
01457 
01458     def create_unknown_cover(self, plugin):
01459         # set the unknown cover to the requester to make comparisons
01460         self._requester.unknown_cover = self.unknown_cover
01461 
01462     def create_cover(self, image):
01463         return Cover(self.cover_size, image)
01464 
01465     def coverart_added_callback(self, ext_db, key, path, pixbuf):
01466         # use the name to get the album and update it's cover
01467         if pixbuf:
01468             coverobject = self._manager.model.get_from_ext_db_key(key)
01469 
01470             if coverobject:
01471                 coverobject.cover = self.create_cover(path)
01472 
01473     def load_cover(self, coverobject):
01474         '''
01475         Tries to load an Album's cover. If no cover is found upon lookup,
01476         the unknown cover is used.
01477         This method doesn't actively tries to find a cover, for that you should
01478         use the search_cover method.
01479 
01480         :param album: `Album` for which load the cover.
01481         '''
01482         # create a key and look for the art location
01483         key = coverobject.create_ext_db_key()
01484         art_location = self.cover_db.lookup(key)
01485 
01486         # try to create a cover
01487         if art_location:
01488             coverobject.cover = self.create_cover(art_location)
01489         else:
01490             coverobject.cover = self.unknown_cover
01491 
01492     def load_covers(self):
01493         '''
01494         Loads all the covers for the model's albums.
01495         '''
01496         # get all the coverobjects
01497         coverobjects = self._manager.model.get_all()
01498 
01499         self._load_covers(iter(coverobjects), total=len(coverobjects), progress=0.)
01500 
01501     def search_covers(self, coverobjects=None, callback=lambda *_: None):
01502         '''
01503         Request all the albums' covers, one by one, periodically calling a
01504         callback to inform the status of the process.
01505         The callback should accept one argument: the album which cover is
01506         being requested. When the argument passed is None, it means the
01507         process has finished.
01508 
01509         :param albums: `list` of `Album` for which look for covers.
01510         :param callback: `callable` to periodically inform when an album's
01511             cover is being searched.
01512         '''
01513         if not check_lastfm(self.force_lastfm_check):
01514             # display error message and quit
01515             dialog = Gtk.MessageDialog(None,
01516                                        Gtk.DialogFlags.MODAL,
01517                                        Gtk.MessageType.INFO,
01518                                        Gtk.ButtonsType.OK,
01519                                        _("Enable LastFM plugin and log in first"))
01520 
01521             dialog.run()
01522             dialog.destroy()
01523 
01524             return
01525 
01526         if coverobjects is None:
01527             self._requester.replace_queue(
01528                 list(self._manager.model.get_all()), callback)
01529         else:
01530             self._requester.add_to_queue(coverobjects, callback)
01531 
01532     def cancel_cover_request(self):
01533         '''
01534         Cancel the current cover request, if there is one running.
01535         '''
01536         self._requester.stop()
01537 
01538     def update_pixbuf_cover(self, coverobject, pixbuf):
01539         pass
01540 
01541     def update_cover(self, coverobject, pixbuf=None, uri=None):
01542         '''
01543         Updates the cover database, inserting the pixbuf as the cover art for
01544         all the entries on the album.
01545         In the case a uri is given instead of the pixbuf, it will first try to
01546         retrieve an image from the uri, then recall this method with the
01547         obtained pixbuf.
01548 
01549         :param album: `Album` for which the cover is.
01550         :param pixbuf: `GkdPixbuf.Pixbuf` to use as a cover.
01551         :param uri: `str` from where we should try to retrieve an image.
01552         '''
01553         if pixbuf:
01554             self.update_pixbuf_cover(coverobject, pixbuf)
01555         elif uri:
01556             parsed = rb3compat.urlparse(uri)
01557 
01558             if parsed.scheme == 'file':
01559                 # local file, load it on a pixbuf and assign it
01560                 path = rb3compat.url2pathname(uri.strip()).replace('file://', '')
01561 
01562                 if os.path.exists(path):
01563                     cover = GdkPixbuf.Pixbuf.new_from_file(path)
01564                     self.update_cover(coverobject, cover)
01565             else:
01566                 # assume is a remote uri and we have to retrieve the data
01567                 def cover_update(data, coverobject):
01568                     # save the cover on a temp file and open it as a pixbuf
01569                     with tempfile.NamedTemporaryFile(mode='wb') as tmp:
01570                         try:
01571                             tmp.write(data)
01572                             tmp.flush()
01573                             cover = GdkPixbuf.Pixbuf.new_from_file(tmp.name)
01574 
01575                             # set the new cover
01576                             self.update_cover(coverobject, cover)
01577                         except:
01578                             print("The URI doesn't point to an image or " + \
01579                                   "the image couldn't be opened.")
01580 
01581                 async = rb.Loader()
01582                 async.get_url(uri, cover_update, coverobject)
01583 
01584 
01585 class AlbumCoverManager(CoverManager):
01586     # properties
01587     add_shadow = GObject.property(type=bool, default=False)
01588     shadow_image = GObject.property(type=str, default="above")
01589 
01590     def __init__(self, plugin, album_manager):
01591         self.cover_db = RB.ExtDB(name='album-art')
01592         super(AlbumCoverManager, self).__init__(plugin, album_manager)
01593 
01594         self.album_manager = album_manager
01595         self._connect_properties()
01596         self._connect_signals(plugin)
01597 
01598         # create unknown cover and shadow for covers
01599         self.create_unknown_cover(plugin)
01600 
01601     def _connect_signals(self, plugin):
01602         self.connect('notify::cover-size', self._on_cover_size_changed)
01603         self.connect('notify::add-shadow', self._on_add_shadow_changed, plugin)
01604         self.connect('notify::shadow-image', self._on_add_shadow_changed,
01605                      plugin)
01606 
01607     def _connect_properties(self):
01608         gs = GSetting()
01609         setting = gs.get_setting(gs.Path.PLUGIN)
01610 
01611         setting.bind(gs.PluginKey.COVER_SIZE, self, 'cover_size',
01612                      Gio.SettingsBindFlags.GET)
01613         setting.bind(gs.PluginKey.ADD_SHADOW, self, 'add_shadow',
01614                      Gio.SettingsBindFlags.GET)
01615         setting.bind(gs.PluginKey.SHADOW_IMAGE, self, 'shadow_image',
01616                      Gio.SettingsBindFlags.GET)
01617 
01618     def create_unknown_cover(self, plugin):
01619         # create the unknown cover
01620         self._shadow = Shadow(self.cover_size,
01621                               rb.find_plugin_file(plugin, 'img/album-shadow-%s.png' %
01622                                                           self.shadow_image))
01623         self.unknown_cover = self.create_cover(
01624             rb.find_plugin_file(plugin, 'img/rhythmbox-missing-artwork.svg'))
01625 
01626         super(AlbumCoverManager, self).create_unknown_cover(plugin)
01627 
01628     def create_cover(self, image):
01629         if self.add_shadow:
01630             cover = ShadowedCover(self._shadow, image)
01631         else:
01632             cover = Cover(self.cover_size, image)
01633 
01634         return cover
01635 
01636     def _on_add_shadow_changed(self, obj, prop, plugin):
01637         # update the unknown_cover
01638         self.create_unknown_cover(plugin)
01639 
01640         # recreate all the covers
01641         self.load_covers()
01642 
01643     def _on_cover_size_changed(self, *args):
01644         '''
01645         Updates the showing albums cover size.
01646         '''
01647         # update the shadow
01648         self._shadow.resize(self.cover_size)
01649 
01650         # update coverview item width
01651         self.update_item_width()
01652 
01653         # update the album's covers
01654         albums = self.album_manager.model.get_all()
01655 
01656         self._resize_covers(iter(albums), total=len(albums), progress=0.)
01657 
01658     def update_item_width(self):
01659         self.album_manager.current_view.resize_icon(self.cover_size)
01660 
01661     def update_pixbuf_cover(self, coverobject, pixbuf):
01662         # if it's a pixbuf, assign it to all the artist for the album
01663         key = RB.ExtDBKey.create_storage('album', coverobject.name)
01664         key.add_field('artist', coverobject.artist)
01665 
01666         self.cover_db.store(key, RB.ExtDBSourceType.USER_EXPLICIT,
01667                             pixbuf)
01668 
01669         for artist in coverobject.artists.split(', '):
01670             key = RB.ExtDBKey.create_storage('album', coverobject.name)
01671             key.add_field('artist', artist)
01672 
01673             self.cover_db.store(key, RB.ExtDBSourceType.USER_EXPLICIT,
01674                                 pixbuf)
01675 
01676     @idle_iterator
01677     def _resize_covers(self):
01678         def process(coverobject, data):
01679             coverobject.cover.resize(self.cover_size)
01680 
01681         def finish(data):
01682             self.album_manager.progress = 1
01683             self.emit('load-finished')
01684 
01685         def error(exception):
01686             print("Error while resizing covers: " + str(exception))
01687 
01688         def after(data):
01689             data['progress'] += COVER_LOAD_CHUNK
01690 
01691             # update the progress
01692             self.album_manager.progress = data['progress'] / data['total']
01693 
01694         return COVER_LOAD_CHUNK, process, after, error, finish
01695 
01696 
01697 class TextManager(GObject.Object):
01698     '''
01699     Manager that keeps control of the text options for the model's markup text.
01700     It takes care of creating the text for the model when requested to do it.
01701 
01702     :param album_manager: `AlbumManager` responsible for this manager.
01703     '''
01704     # properties
01705     display_text_ellipsize_enabled = GObject.property(type=bool, default=False)
01706     display_text_ellipsize_length = GObject.property(type=int, default=0)
01707     display_font_size = GObject.property(type=int, default=0)
01708 
01709     def __init__(self, album_manager):
01710         super(TextManager, self).__init__()
01711 
01712         self._album_manager = album_manager
01713         self._current_view = self._album_manager.current_view
01714 
01715         # connect properties and signals
01716         self._connect_signals()
01717         self._connect_properties()
01718 
01719     def _connect_signals(self):
01720         '''
01721         Connects the loader to all the needed signals for it to work.
01722         '''
01723         # connect signals for the loader properties
01724         self.connect('notify::display-text-ellipsize-enabled',
01725                      self._on_notify_display_text_ellipsize)
01726         self.connect('notify::display-text-ellipsize-length',
01727                      self._on_notify_display_text_ellipsize)
01728         self.connect('notify::display-font-size',
01729                      self._on_notify_display_text_ellipsize)
01730 
01731         self._album_manager.model.connect('generate-tooltip',
01732                                           self._generate_tooltip)
01733         self._album_manager.model.connect('generate-markup',
01734                                           self._generate_markup_text)
01735 
01736     def _connect_properties(self):
01737         '''
01738         Connects the loader properties to the saved preferences.
01739         '''
01740         gs = GSetting()
01741         setting = gs.get_setting(gs.Path.PLUGIN)
01742 
01743         setting.bind(gs.PluginKey.DISPLAY_TEXT_ELLIPSIZE, self,
01744                      'display_text_ellipsize_enabled', Gio.SettingsBindFlags.GET)
01745         setting.bind(gs.PluginKey.DISPLAY_TEXT_ELLIPSIZE_LENGTH, self,
01746                      'display_text_ellipsize_length',
01747                      Gio.SettingsBindFlags.GET)
01748         setting.bind(gs.PluginKey.DISPLAY_FONT_SIZE, self, 'display_font_size',
01749                      Gio.SettingsBindFlags.GET)
01750 
01751     def _on_notify_display_text_ellipsize(self, *args):
01752         '''
01753         Callback called when one of the properties related with the ellipsize
01754         option is changed.
01755         '''
01756         self._album_manager.model.recreate_text()
01757 
01758     def _generate_tooltip(self, model, album):
01759         '''
01760         Utility function that creates the tooltip for this album to set into
01761         the model.
01762         '''
01763         return cgi.escape(rb3compat.unicodeencode(_('%s by %s'), 'utf-8') % (album.name,
01764                                                                              album.artists))
01765 
01766     def _generate_markup_text(self, model, album):
01767         '''
01768         Utility function that creates the markup text for this album to set
01769         into the model.
01770         '''
01771         # we use unicode to avoid problems with non ascii albums
01772         name = rb3compat.unicodestr(album.name, 'utf-8')
01773         artist = rb3compat.unicodestr(album.artist, 'utf-8')
01774 
01775         if self.display_text_ellipsize_enabled:
01776             ellipsize = self.display_text_ellipsize_length
01777 
01778             if len(name) > ellipsize:
01779                 name = name[:ellipsize] + '...'
01780 
01781             if len(artist) > ellipsize:
01782                 artist = artist[:ellipsize] + '...'
01783 
01784         name = rb3compat.unicodeencode(name, 'utf-8')
01785         artist = rb3compat.unicodeencode(artist, 'utf-8')
01786 
01787         # escape odd chars
01788         artist = GLib.markup_escape_text(artist)
01789         name = GLib.markup_escape_text(name)
01790 
01791         # markup format
01792         markup = "<span font='%d'><b>%s</b>\n<i>%s</i></span>"
01793         return markup % (self.display_font_size, name, artist)
01794 
01795 
01796 class AlbumManager(GObject.Object):
01797     '''
01798     Main construction that glues together the different managers, the loader
01799     and the model. It takes care of initializing all the system.
01800 
01801     :param plugin: `Peas.PluginInfo` instance.
01802     :param current_view: `AlbumView` where the album's cover are shown.
01803     '''
01804     # singleton instance
01805     instance = None
01806 
01807     # properties
01808     progress = GObject.property(type=float, default=0)
01809 
01810     # signals
01811     __gsignals__ = {
01812         'sort': (GObject.SIGNAL_RUN_LAST, None, (object,))
01813     }
01814 
01815 
01816     def __init__(self, plugin, current_view):
01817         super(AlbumManager, self).__init__()
01818 
01819         self.current_view = current_view
01820         self.db = plugin.shell.props.db
01821 
01822         self.model = AlbumsModel()
01823 
01824         # initialize managers
01825         self.loader = AlbumLoader(self)
01826         self.cover_man = AlbumCoverManager(plugin, self)
01827         from coverart_artistview import ArtistManager
01828 
01829         self.artist_man = ArtistManager(plugin, self, plugin.shell)
01830         self.text_man = TextManager(self)
01831         self._show_policy = current_view.show_policy.initialise(self)
01832 
01833         # connect signals
01834         self._connect_signals()
01835 
01836     def _connect_signals(self):
01837         '''
01838         Connects the manager to all the needed signals for it to work.
01839         '''
01840         # connect signal to the loader so it shows the albums when it finishes
01841         self._load_finished_id = self.loader.connect('model-load-finished',
01842                                                      self._load_finished_callback)
01843         self.connect('sort', self._sort_album)
01844 
01845     def _sort_album(self, widget, param):
01846         toolbar_type = param
01847 
01848         if not toolbar_type or toolbar_type == "album":
01849             self.model.sort()
01850 
01851     def _load_finished_callback(self, *args):
01852         self.artist_man.loader.load_artists()
01853         self.cover_man.load_covers()
 All Classes Functions