CoverArt Browser
v2.0
Browse your cover-art albums in Rhythmbox
|
00001 # -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*- 00002 # 00003 # Copyright (C) 2012 - fossfreedom 00004 # Copyright (C) 2012 - Agustin Carrasco 00005 # 00006 # This program is free software; you can redistribute it and/or modify 00007 # it under the terms of the GNU General Public License as published by 00008 # the Free Software Foundation; either version 2, or (at your option) 00009 # any later version. 00010 # 00011 # This program is distributed in the hope that it will be useful, 00012 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00013 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00014 # GNU General Public License for more details. 00015 # 00016 # You should have received a copy of the GNU General Public License 00017 # along with this program; if not, write to the Free Software 00018 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 00019 00020 import 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.)