CoverArt Browser  v2.0
Browse your cover-art albums in Rhythmbox
/home/foss/Downloads/coverart-browser/coverart_playlists.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 urllib.parse
00021 import json
00022 import os
00023 import random
00024 
00025 from gi.repository import RB
00026 from gi.repository import Gtk
00027 
00028 from coverart_utils import idle_iterator
00029 import rb
00030 
00031 
00032 LOAD_CHUNK = 50
00033 
00034 
00035 class WebPlaylist(object):
00036     MAX_TRACKS_TO_ADD = 3  # number of tracks to add to a source for each fetch
00037     MIN_TRACKS_TO_FETCH = 5  # number of tracks in source before a fetch will be required
00038     TOTAL_TRACKS_REMEMBERED = 25  # total number of tracks for all artists before a fetch is allowed
00039     MAX_TRACKS_PER_ARTIST = 3  # number of tracks allowed to be remembered per artist
00040 
00041     def __init__(self, shell, source, playlist_name):
00042 
00043         self.shell = shell
00044         #lets fill up the queue with artists
00045         self.candidate_artist = {}
00046         self.shell.props.shell_player.connect('playing-song-changed', self.playing_song_changed)
00047         self.source = source
00048         self.search_entry = None
00049         self.playlist_started = False
00050         self.played_artist = {}
00051         self.tracks_not_played = 0
00052         # cache for artist information: valid for a month, can be used indefinitely
00053         # if offline, discarded if unused for six months
00054         self.info_cache = rb.URLCache(name=playlist_name,
00055                                       path=os.path.join('coverart_browser', playlist_name),
00056                                       refresh=30,
00057                                       discard=180)
00058         self.info_cache.clean()
00059 
00060     def playing_song_changed(self, player, entry):
00061         if not entry:
00062             return
00063 
00064         if player.get_playing_source() != self.source:
00065             self.playlist_started = False
00066             self.played_artist.clear()
00067             self.tracks_not_played = 0
00068 
00069         if self.playlist_started and len(self.source.props.query_model) < self.MIN_TRACKS_TO_FETCH:
00070             self.start(entry)
00071 
00072     def start(self, seed_entry, reinitialise=False):
00073         artist = seed_entry.get_string(RB.RhythmDBPropType.ARTIST)
00074 
00075         if reinitialise:
00076             self.played_artist.clear()
00077             self.tracks_not_played = 0
00078             self.playlist_started = False
00079 
00080             player = self.shell.props.shell_player
00081             _, is_playing = player.get_playing()
00082 
00083             if is_playing:
00084                 player.stop()
00085 
00086             for row in self.source.props.query_model:
00087                 self.source.props.query_model.remove_entry(row[0])
00088 
00089         if self.tracks_not_played > self.TOTAL_TRACKS_REMEMBERED:
00090             print(("we have plenty of tracks to play yet - no need to fetch more %d", self.tracks_not_played))
00091             self.add_tracks_to_source()
00092             return
00093 
00094         search_artist = urllib.parse.quote(artist.encode("utf8"))
00095         if search_artist in self.played_artist:
00096             print("we have already searched for that artist")
00097             return
00098 
00099         self.search_entry = seed_entry
00100         self.played_artist[search_artist] = True
00101 
00102         self.playlist_started = True
00103         self._running = False
00104         self._start_process()
00105 
00106     def _start_process(self):
00107         if not self._running:
00108             self._running = True
00109             self.search_website()
00110 
00111     def search_website(self):
00112         pass
00113 
00114     def _clear_next(self):
00115         self.search_artists = ""
00116         self._running = False
00117 
00118     @idle_iterator
00119     def _load_albums(self):
00120         def process(row, data):
00121             entry = data['model'][row.path][0]
00122 
00123             lookup = entry.get_string(RB.RhythmDBPropType.ARTIST_FOLDED)
00124             lookup_title = entry.get_string(RB.RhythmDBPropType.TITLE_FOLDED)
00125 
00126             if lookup in self.artist and \
00127                             lookup_title in \
00128                             self.artist[lookup]:
00129 
00130                 if lookup not in self.candidate_artist:
00131                     self.candidate_artist[lookup] = []
00132 
00133                 # N.B. every artist has an array of dicts with a known format of track & add-to-source elements
00134                 # the following extracts the track-title and add-to-source to form a dict of track-title and a value
00135                 # of the add-to-source
00136                 d = dict((i['track-title'], i['add-to-source']) for i in self.candidate_artist[lookup])
00137                 if len(d) < self.MAX_TRACKS_PER_ARTIST and lookup_title not in d:
00138                     # we only append a max of three tracks to each artist
00139                     self.candidate_artist[lookup].append({
00140                         'track': entry,
00141                         'add-to-source': False,
00142                         'track-title': lookup_title})
00143                     self.tracks_not_played = self.tracks_not_played + 1
00144 
00145 
00146         def after(data):
00147             # update the progress
00148             pass
00149 
00150         def error(exception):
00151             print(('Error processing entries: ' + str(exception)))
00152 
00153         def finish(data):
00154 
00155             self.add_tracks_to_source()
00156             self._clear_next()
00157 
00158         return LOAD_CHUNK, process, after, error, finish
00159 
00160     def display_error_message(self):
00161         dialog = Gtk.MessageDialog(None,
00162                                    Gtk.DialogFlags.MODAL,
00163                                    Gtk.MessageType.INFO,
00164                                    Gtk.ButtonsType.OK,
00165                                    _("No matching tracks have been found"))
00166 
00167         dialog.run()
00168         dialog.destroy()
00169 
00170     def add_tracks_to_source(self):
00171         entries = []
00172         for artist in self.candidate_artist:
00173 
00174             d = dict((i['track'], (self.candidate_artist[artist].index(i),
00175                                    i['add-to-source'],
00176                                    artist)) for i in self.candidate_artist[artist])
00177 
00178             for entry, elements in d.items():
00179                 element_pos, add_to_source, artist = elements
00180                 if not add_to_source:
00181                     entries.append({entry: elements})
00182 
00183         random.shuffle(entries)
00184 
00185         count = 0
00186         for row in entries:
00187             print(row)
00188             entry, elements = list(row.items())[0]
00189             element_pos, add_to_source, artist = elements
00190             self.source.add_entry(entry, -1)
00191             self.candidate_artist[artist][element_pos]['add-to-source'] = True
00192 
00193             count = count + 1
00194             self.tracks_not_played = self.tracks_not_played - 1
00195             if count == self.MAX_TRACKS_TO_ADD:
00196                 break
00197 
00198         player = self.shell.props.shell_player
00199 
00200         _, is_playing = player.get_playing()
00201 
00202         if len(self.source.props.query_model) > 0 and not is_playing:
00203             player.play_entry(self.source.props.query_model[0][0], self.source)
00204 
00205 
00206 class LastFMTrackPlaylist(WebPlaylist):
00207     def __init__(self, shell, source):
00208         WebPlaylist.__init__(self, shell, source, "lastfm_trackplaylist")
00209 
00210     def search_website(self):
00211         # unless already cached - directly fetch from lastfm similar track information
00212         apikey = "844353bce568b93accd9ca47674d6c3e"
00213         url = "http://ws.audioscrobbler.com/2.0/?method=track.getsimilar&api_key={0}&artist={1}&track={2}&format=json"
00214 
00215         artist = self.search_entry.get_string(RB.RhythmDBPropType.ARTIST)
00216         title = self.search_entry.get_string(RB.RhythmDBPropType.TITLE)
00217         artist = urllib.parse.quote(artist.encode("utf8"))
00218         title = urllib.parse.quote(title.encode("utf8"))
00219         formatted_url = url.format(urllib.parse.quote(apikey),
00220                                    artist,
00221                                    title)
00222 
00223         print(formatted_url)
00224         cachekey = "artist:%s:title:%s" % (artist, title)
00225         self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
00226 
00227     def similar_info_cb(self, data, _):
00228 
00229         if not data:
00230             print("nothing to do")
00231             self.display_error_message()
00232             self._clear_next()
00233             return
00234 
00235         similar = json.loads(data.decode('utf-8'))
00236 
00237         # loop through the response and find all titles for the artists returned
00238         self.artist = {}
00239 
00240         if 'similartracks' not in similar:
00241             print("No matching data returned from LastFM")
00242             self.display_error_message()
00243             self._clear_next()
00244             return
00245         for song in similar['similartracks']['track']:
00246             name = RB.search_fold(song['artist']['name'])
00247             if name not in self.artist:
00248                 self.artist[name] = []
00249 
00250             self.artist[name].append(RB.search_fold(song['name']))
00251 
00252         if len(self.artist) == 0:
00253             print("no artists returned")
00254             self._clear_next()
00255             return
00256 
00257         # loop through every track - see if the track contains the artist & title
00258         # if yes then this is a candidate similar track to remember
00259 
00260         query_model = self.shell.props.library_source.props.base_query_model
00261 
00262         self._load_albums(iter(query_model), albums={}, model=query_model,
00263                           total=len(query_model), progress=0.)
00264 
00265 
00266 class EchoNestPlaylist(WebPlaylist):
00267     def __init__(self, shell, source):
00268         WebPlaylist.__init__(self, shell, source, "echonest_playlist")
00269 
00270     def search_website(self):
00271         # unless already cached - directly fetch from echonest similar artist information
00272         apikey = "N685TONJGZSHBDZMP"
00273         url = "http://developer.echonest.com/api/v4/playlist/basic?api_key={0}&artist={1}&format=json&results=100&type=artist-radio&limited_interactivity=true"
00274 
00275         artist = self.search_entry.get_string(RB.RhythmDBPropType.ARTIST)
00276         artist = urllib.parse.quote(artist.encode("utf8"))
00277         formatted_url = url.format(urllib.parse.quote(apikey),
00278                                    artist)
00279 
00280         print(formatted_url)
00281         cachekey = "artist:%s" % artist
00282         self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
00283 
00284     def similar_info_cb(self, data, _):
00285 
00286         if not data:
00287             print("nothing to do")
00288             self.display_error_message()
00289             self._clear_next()
00290             return
00291 
00292         similar = json.loads(data.decode('utf-8'))
00293 
00294         # loop through the response and find all titles for the artists returned
00295         self.artist = {}
00296 
00297         if 'songs' not in similar['response']:
00298             print("No matching data returned from EchoNest")
00299             self.display_error_message()
00300             self._clear_next()
00301             return
00302         for song in similar['response']['songs']:
00303             name = RB.search_fold(song['artist_name'])
00304             if name not in self.artist:
00305                 self.artist[name] = []
00306 
00307             self.artist[name].append(RB.search_fold(song['title']))
00308 
00309         if len(self.artist) == 0:
00310             print("no artists returned")
00311             self._clear_next()
00312             return
00313 
00314         # loop through every track - see if the track contains the artist & title
00315         # if yes then this is a candidate similar track to remember
00316 
00317         query_model = self.shell.props.library_source.props.base_query_model
00318 
00319         self._load_albums(iter(query_model), albums={}, model=query_model,
00320                           total=len(query_model), progress=0.)
00321 
00322 
00323 class EchoNestGenrePlaylist(WebPlaylist):
00324     def __init__(self, shell, source):
00325         WebPlaylist.__init__(self, shell, source, "echonest_genre_playlist")
00326 
00327     def search_website(self):
00328         # unless already cached - directly fetch from echonest similar artist information
00329         apikey = "N685TONJGZSHBDZMP"
00330         url = "http://developer.echonest.com/api/v4/playlist/basic?api_key={0}&genre={1}&format=json&results=100&type=genre-radio&limited_interactivity=true"
00331 
00332         genre = self.search_entry.get_string(RB.RhythmDBPropType.GENRE).lower()
00333         genre = urllib.parse.quote(genre.encode("utf8"))
00334         formatted_url = url.format(urllib.parse.quote(apikey),
00335                                    genre)
00336 
00337         print(formatted_url)
00338         cachekey = "genre:%s" % genre
00339         self.info_cache.fetch(cachekey, formatted_url, self.similar_info_cb, None)
00340 
00341     def similar_info_cb(self, data, _):
00342 
00343         if not data:
00344             print("nothing to do")
00345             self.display_error_message()
00346             self._clear_next()
00347             return
00348 
00349         similar = json.loads(data.decode('utf-8'))
00350 
00351         # loop through the response and find all titles for the artists returned
00352         self.artist = {}
00353 
00354         if 'songs' not in similar['response']:
00355             print("No matching data returned from EchoNest")
00356             self.display_error_message()
00357             self._clear_next()
00358             return
00359         for song in similar['response']['songs']:
00360             name = RB.search_fold(song['artist_name'])
00361             if name not in self.artist:
00362                 self.artist[name] = []
00363 
00364             self.artist[name].append(RB.search_fold(song['title']))
00365 
00366         if len(self.artist) == 0:
00367             print("no artists returned")
00368             self._clear_next()
00369             return
00370 
00371         # loop through every track - see if the track contains the artist & title
00372         # if yes then this is a candidate similar track to remember
00373 
00374         query_model = self.shell.props.library_source.props.base_query_model
00375 
00376         self._load_albums(iter(query_model), albums={}, model=query_model,
00377                           total=len(query_model), progress=0.)
 All Classes Functions