1# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*- 2# 3# Copyright (C) 2009 John Iacona 4# 5# This program is free software; you can redistribute it and/or modify 6# it under the terms of the GNU General Public License as published by 7# the Free Software Foundation; either version 2, or (at your option) 8# any later version. 9# 10# The Rhythmbox authors hereby grant permission for non-GPL compatible 11# GStreamer plugins to be used and distributed together with GStreamer 12# and Rhythmbox. This permission is above and beyond the permissions granted 13# by the GPL license by which Rhythmbox is covered. If you modify this code 14# you may extend this exception to your version of the code, but you are not 15# obligated to do so. If you do not wish to do so, delete this exception 16# statement from your version. 17# 18# This program is distributed in the hope that it will be useful, 19# but WITHOUT ANY WARRANTY; without even the implied warranty of 20# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 21# GNU General Public License for more details. 22# 23# You should have received a copy of the GNU General Public License 24# along with this program; if not, write to the Free Software 25# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 26 27import os 28import cgi 29import urllib.request, urllib.parse 30from mako.template import Template 31import json 32 33import LastFM 34 35import rb 36from gi.repository import RB 37from gi.repository import GObject, Gtk 38from gi.repository import WebKit 39 40import gettext 41gettext.install('rhythmbox', RB.locale_dir()) 42 43class AlbumTab (GObject.GObject): 44 45 __gsignals__ = { 46 'switch-tab' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, 47 (GObject.TYPE_STRING,)) 48 } 49 50 def __init__ (self, shell, buttons, ds, view): 51 GObject.GObject.__init__ (self) 52 self.shell = shell 53 self.sp = shell.props.shell_player 54 self.db = shell.props.db 55 self.buttons = buttons 56 57 self.button = Gtk.ToggleButton (label=_("Albums")) 58 self.ds = ds 59 self.view = view 60 self.artist = None 61 self.active = False 62 63 self.button.show() 64 self.button.set_relief (Gtk.ReliefStyle.NONE) 65 self.button.set_focus_on_click(False) 66 self.button.connect ('clicked', 67 lambda button: self.emit ('switch-tab', 'album')) 68 buttons.pack_start (self.button, True, True, 0) 69 70 def activate (self): 71 self.button.set_active(True) 72 self.active = True 73 self.reload () 74 75 def deactivate (self): 76 self.button.set_active(False) 77 self.active = False 78 79 def reload (self): 80 entry = self.sp.get_playing_entry () 81 if entry is None: 82 return None 83 84 artist = entry.get_string (RB.RhythmDBPropType.ARTIST) 85 album = entry.get_string (RB.RhythmDBPropType.ALBUM) 86 if self.active and artist != self.artist: 87 self.view.loading(artist) 88 self.ds.fetch_album_list (artist) 89 else: 90 self.view.load_view() 91 92 self.artist = artist 93 94class AlbumView (GObject.GObject): 95 96 def __init__ (self, shell, plugin, webview, ds): 97 GObject.GObject.__init__ (self) 98 self.webview = webview 99 self.ds = ds 100 self.shell = shell 101 self.plugin = plugin 102 self.file = "" 103 104 plugindir = plugin.plugin_info.get_data_dir() 105 self.basepath = "file://" + urllib.request.pathname2url (plugindir) 106 107 self.load_tmpl () 108 self.connect_signals () 109 110 def load_view (self): 111 self.webview.load_string(self.file, 'text/html', 'utf-8', self.basepath) 112 113 def connect_signals (self): 114 self.ds.connect('albums-ready', self.album_list_ready) 115 116 def loading (self, current_artist): 117 self.loading_file = self.loading_template.render ( 118 artist = current_artist, 119 # Translators: 'top' here means 'most popular'. %s is replaced by the artist name. 120 info = _("Loading top albums for %s") % current_artist, 121 song = "", 122 basepath = self.basepath) 123 self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath) 124 125 def load_tmpl (self): 126 self.path = rb.find_plugin_file (self.plugin, 'tmpl/album-tmpl.html') 127 self.loading_path = rb.find_plugin_file (self.plugin, 'tmpl/loading.html') 128 self.album_template = Template (filename = self.path) 129 self.loading_template = Template (filename = self.loading_path) 130 self.styles = self.basepath + '/tmpl/main.css' 131 132 def album_list_ready (self, ds): 133 self.file = self.album_template.render (error = ds.get_error(), 134 albums = ds.get_top_albums(), 135 artist = ds.get_artist(), 136 datasource = LastFM.datasource_link (self.basepath), 137 stylesheet = self.styles) 138 self.load_view () 139 140 141class AlbumDataSource (GObject.GObject): 142 143 __gsignals__ = { 144 'albums-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) 145 } 146 147 def __init__ (self, info_cache, ranking_cache): 148 GObject.GObject.__init__ (self) 149 self.albums = None 150 self.error = None 151 self.artist = None 152 self.max_albums_fetched = 8 153 self.fetching = 0 154 self.info_cache = info_cache 155 self.ranking_cache = ranking_cache 156 157 def get_artist (self): 158 return self.artist 159 160 def get_error (self): 161 return self.error 162 163 def fetch_album_list (self, artist): 164 if LastFM.user_has_account() is False: 165 self.error = LastFM.NO_ACCOUNT_ERROR 166 self.emit ('albums-ready') 167 return 168 169 self.artist = artist 170 qartist = urllib.parse.quote_plus(artist) 171 self.error = None 172 url = "%sartist.gettopalbums&artist=%s&api_key=%s&format=json" % ( 173 LastFM.URL_PREFIX, qartist, LastFM.API_KEY) 174 cachekey = 'lastfm:artist:gettopalbumsjson:%s' % qartist 175 self.ranking_cache.fetch(cachekey, url, self.parse_album_list, artist) 176 177 def parse_album_list (self, data, artist): 178 if data is None: 179 print("Nothing fetched for %s top albums" % artist) 180 return False 181 182 try: 183 parsed = json.loads(data.decode("utf-8")) 184 except Exception as e: 185 print("Error parsing album list: %s" % e) 186 return False 187 188 self.error = parsed.get('error') 189 if self.error: 190 self.emit ('albums-ready') 191 return False 192 193 albums = parsed['topalbums'].get('album', []) 194 if len(albums) == 0: 195 self.error = "No albums found for %s" % artist 196 self.emit('albums-ready') 197 return True 198 199 self.albums = [] 200 albums = parsed['topalbums'].get('album', [])[:self.max_albums_fetched] 201 self.fetching = len(albums) 202 for i, a in enumerate(albums): 203 images = [img['#text'] for img in a.get('image', [])] 204 self.albums.append({'title': a.get('name'), 'images': images[:3]}) 205 self.fetch_album_info(artist, a.get('name'), i) 206 207 return True 208 209 def get_top_albums (self): 210 return self.albums 211 212 def fetch_album_info (self, artist, album, index): 213 qartist = urllib.parse.quote_plus(artist) 214 qalbum = urllib.parse.quote_plus(album) 215 cachekey = "lastfm:album:getinfojson:%s:%s" % (qartist, qalbum) 216 url = "%salbum.getinfo&artist=%s&album=%s&api_key=%s&format=json" % ( 217 LastFM.URL_PREFIX, qartist, qalbum, LastFM.API_KEY) 218 self.info_cache.fetch(cachekey, url, self.parse_album_info, album, index) 219 220 def parse_album_info (self, data, album, index): 221 rv = True 222 try: 223 parsed = json.loads(data.decode('utf-8')) 224 self.albums[index]['id'] = parsed['album']['id'] 225 226 for k in ('releasedate', 'summary'): 227 self.albums[index][k] = parsed['album'].get(k) 228 229 tracklist = [] 230 tracks = parsed['album']['tracks'].get('track', []) 231 for i, t in enumerate(tracks): 232 title = t['name'] 233 duration = int(t['duration']) 234 tracklist.append((i, title, duration)) 235 236 self.albums[index]['tracklist'] = tracklist 237 self.albums[index]['duration'] = sum([t[2] for t in tracklist]) 238 239 except Exception as e: 240 print("Error parsing album tracklist: %s" % e) 241 rv = False 242 243 self.fetching -= 1 244 print("%s albums left to process" % self.fetching) 245 if self.fetching == 0: 246 self.emit('albums-ready') 247 248 return rv 249