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) 2014 fossfreedom 00004 # this module has been heavily modifed from rhythmbox context plugin 00005 # Copyright (C) 2009 John Iacona 00006 # 00007 # This program is free software; you can redistribute it and/or modify 00008 # it under the terms of the GNU General Public License as published by 00009 # the Free Software Foundation; either version 2, or (at your option) 00010 # any later version. 00011 # 00012 # This program is distributed in the hope that it will be useful, 00013 # but WITHOUT ANY WARRANTY; without even the implied warranty of 00014 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00015 # GNU General Public License for more details. 00016 # 00017 # You should have received a copy of the GNU General Public License 00018 # along with this program; if not, write to the Free Software 00019 # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 00020 00021 import os 00022 import urllib.request 00023 import urllib.parse 00024 import json 00025 import gettext 00026 00027 from mako.template import Template 00028 from gi.repository import WebKit 00029 from gi.repository import GObject 00030 from gi.repository import Gtk 00031 from gi.repository import Gdk 00032 from gi.repository import GLib 00033 from gi.repository import RB 00034 from gi.repository import Gio 00035 00036 import rb 00037 import rb_lastfm as LastFM # from coverart-search-providers 00038 from coverart_utils import get_stock_size 00039 from coverart_browser_prefs import GSetting 00040 from coverart_browser_prefs import CoverLocale 00041 from coverart_utils import create_button_image 00042 00043 00044 gettext.install('rhythmbox', RB.locale_dir()) 00045 00046 00047 def artist_exceptions(artist): 00048 exceptions = ['various'] 00049 00050 for exception in exceptions: 00051 if exception in artist.lower(): 00052 return True 00053 00054 return False 00055 00056 00057 def lastfm_datasource_link(path): 00058 return "<a href='http://last.fm/'><img src='%s/img/lastfm.png'></a>" % path 00059 00060 00061 LASTFM_NO_ACCOUNT_ERROR = _( 00062 "Enable LastFM plugin and log in first") 00063 00064 00065 class ArtistInfoWebView(WebKit.WebView): 00066 def __init(self, *args, **kwargs): 00067 super(ArtistInfoWebView, self).__init__(*args, **kwargs) 00068 00069 def initialise(self, source, shell): 00070 self.source = source 00071 self.shell = shell 00072 00073 self.connect("navigation-requested", self.navigation_request_cb) 00074 self.connect("notify::title", self.view_title_change) 00075 00076 def view_title_change(self, webview, param): 00077 print ("view_title_change") 00078 title = webview.get_title() 00079 00080 if title: 00081 print ("title %s" % title) 00082 args = json.loads(title) 00083 artist = args['artist'] 00084 00085 if args['toggle']: 00086 self.source.album_manager.model.replace_filter('similar_artist', artist) 00087 else: 00088 self.source.album_manager.model.remove_filter('similar_artist') 00089 else: 00090 print ("removing filter") 00091 self.source.album_manager.model.remove_filter('similar_artist') 00092 print ("end view_title_change") 00093 00094 def navigation_request_cb(self, view, frame, request): 00095 # open HTTP URIs externally. this isn't a web browser. 00096 print ("navigation_request_cb") 00097 if request.get_uri().startswith('http'): 00098 print("opening uri %s" % request.get_uri()) 00099 Gtk.show_uri(self.shell.props.window.get_screen(), request.get_uri(), Gdk.CURRENT_TIME) 00100 00101 return 1 # WEBKIT_NAVIGATION_RESPONSE_IGNORE 00102 else: 00103 return 0 # WEBKIT_NAVIGATION_RESPONSE_ACCEPT 00104 00105 def do_button_release_event(self, *args): 00106 print ("do_release_button") 00107 WebKit.WebView.do_button_release_event(self, *args) 00108 00109 return True 00110 00111 00112 class ArtistInfoPane(GObject.GObject): 00113 __gsignals__ = { 00114 'selected': (GObject.SIGNAL_RUN_LAST, None, 00115 (GObject.TYPE_STRING, GObject.TYPE_STRING)) 00116 } 00117 00118 paned_pos = GObject.property(type=str) 00119 00120 min_paned_pos = 100 00121 00122 def __init__(self, button_box, stack, info_paned, source): 00123 GObject.GObject.__init__(self) 00124 00125 self.ds = {} 00126 self.view = {} 00127 00128 #self.buttons = button_box 00129 self.source = source 00130 self.plugin = source.plugin 00131 self.shell = source.shell 00132 self.info_paned = info_paned 00133 self.current_artist = None 00134 self.current_album_title = None 00135 self.current = 'artist' 00136 self._from_paned_handle = False 00137 00138 self.stack = stack 00139 self.stack.set_transition_type(Gtk.StackTransitionType.SLIDE_LEFT_RIGHT) 00140 stack_switcher = Gtk.StackSwitcher() 00141 stack_switcher.set_stack(self.stack) 00142 self.stack.connect('notify::visible-child-name', self.change_stack) 00143 button_box.pack_start(stack_switcher, False, False, 0) 00144 button_box.show_all() 00145 00146 # cache for artist/album information: valid for a month, can be used indefinitely 00147 # if offline, discarded if unused for six months 00148 self.info_cache = rb.URLCache(name='info', 00149 path=os.path.join('coverart_browser', 'info'), 00150 refresh=30, 00151 discard=180) 00152 # cache for rankings (artist top tracks and top albums): valid for a week, 00153 # can be used for a month if offline 00154 self.ranking_cache = rb.URLCache(name='ranking', 00155 path=os.path.join('coverart_browser', 'ranking'), 00156 refresh=7, 00157 lifetime=30) 00158 00159 self.info_cache.clean() 00160 self.ranking_cache.clean() 00161 00162 self.ds['link'] = LinksDataSource() 00163 self.ds['artist'] = ArtistDataSource(self.info_cache, 00164 self.ranking_cache) 00165 00166 self.view['artist'] = ArtistInfoView() 00167 self.view['artist'].initialise(self.source, 00168 self.shell, 00169 self.plugin, 00170 self.stack, 00171 self.ds['artist'], 00172 self.ds['link']) 00173 00174 self.ds['album'] = AlbumDataSource(self.info_cache, 00175 self.ranking_cache) 00176 self.view['album'] = AlbumInfoView() 00177 self.view['album'].initialise(self.source, 00178 self.shell, 00179 self.plugin, 00180 self.stack, 00181 self.ds['album']) 00182 00183 self.ds['echoartist'] = EchoArtistDataSource( 00184 self.info_cache, 00185 self.ranking_cache) 00186 self.view['echoartist'] = EchoArtistInfoView() 00187 self.view['echoartist'].initialise(self.source, 00188 self.shell, 00189 self.plugin, 00190 self.stack, 00191 self.ds['echoartist'], 00192 self.ds['link']) 00193 00194 self.gs = GSetting() 00195 self.connect_properties() 00196 self.connect_signals() 00197 Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 00198 50, 00199 self._change_paned_pos, 00200 self.source.viewmgr.view_name) 00201 self.view[self.current].activate() 00202 00203 def connect_properties(self): 00204 ''' 00205 Connects the source properties to the saved preferences. 00206 ''' 00207 setting = self.gs.get_setting(self.gs.Path.PLUGIN) 00208 00209 setting.bind( 00210 self.gs.PluginKey.ARTIST_INFO_PANED_POSITION, 00211 self, 00212 'paned-pos', 00213 Gio.SettingsBindFlags.DEFAULT) 00214 00215 def connect_signals(self): 00216 self.tab_cb_ids = [] 00217 00218 # Listen for switch-tab signal from each tab 00219 ''' 00220 for key, value in self.tab.items(): 00221 self.tab_cb_ids.append(( key, 00222 self.tab[key].connect ('switch-tab', 00223 self.change_tab) 00224 )) 00225 ''' 00226 00227 # Listen for selected signal from the views 00228 self.connect('selected', self.select_artist) 00229 00230 # lets remember info paned click 00231 self.info_paned.connect('button_press_event', 00232 self.paned_button_press_callback) 00233 self.info_paned.connect('button-release-event', 00234 self.paned_button_release_callback) 00235 00236 # lets also listen for changes to the view to set the paned position 00237 self.source.viewmgr.connect('new-view', self.on_view_changed) 00238 00239 def on_view_changed(self, widget, view_name): 00240 self._change_paned_pos(view_name) 00241 00242 def _change_paned_pos(self, view_name): 00243 print (self.paned_pos) 00244 paned_positions = eval(self.paned_pos) 00245 00246 found = None 00247 for viewpos in paned_positions: 00248 if view_name in viewpos: 00249 found = viewpos 00250 break 00251 00252 if not found: 00253 return 00254 00255 child_width = int(found.split(":")[1]) 00256 00257 calc_pos = self.source.page.get_allocated_width() - child_width 00258 self.info_paned.set_position(calc_pos) 00259 self.info_paned.set_visible(True) 00260 00261 def _get_child_width(self): 00262 child = self.info_paned.get_child2() 00263 return child.get_allocated_width() 00264 00265 def paned_button_press_callback(self, *args): 00266 print ('paned_button_press_callback') 00267 self._from_paned_handle = True 00268 00269 def paned_button_release_callback(self, widget, *args): 00270 ''' 00271 Callback when the artist paned handle is released from its mouse click. 00272 ''' 00273 if not self._from_paned_handle: 00274 return False 00275 else: 00276 self._from_paned_handle = False 00277 00278 print ("paned_button_release_callback") 00279 child_width = self._get_child_width() 00280 00281 paned_positions = eval(self.paned_pos) 00282 00283 found = None 00284 for viewpos in paned_positions: 00285 if self.source.viewmgr.view_name in viewpos: 00286 found = viewpos 00287 break 00288 00289 if not found: 00290 return True 00291 00292 paned_positions.remove(found) 00293 if child_width <= self.min_paned_pos: 00294 if int(found.split(':')[1]) == 0: 00295 child_width = self.min_paned_pos + 1 00296 calc_pos = self.source.page.get_allocated_width() - child_width 00297 print ("opening") 00298 else: 00299 child_width = 0 00300 calc_pos = self.source.page.get_allocated_width() 00301 print ("smaller") 00302 00303 self.info_paned.set_position(calc_pos) 00304 00305 paned_positions.append(self.source.viewmgr.view_name + ":" + str(child_width)) 00306 00307 self.paned_pos = repr(paned_positions) 00308 print ("End artist_info_paned_button_release_callback") 00309 00310 def select_artist(self, widget, artist, album_title): 00311 print ("artist %s title %s" % (artist, album_title)) 00312 if self._get_child_width() > self.min_paned_pos: 00313 self.view[self.current].reload(artist, album_title) 00314 else: 00315 self.view[self.current].blank_view() 00316 00317 self.current_album_title = album_title 00318 self.current_artist = artist 00319 00320 def change_stack(self, widget, value): 00321 child_name = self.stack.get_visible_child_name() 00322 if child_name and self.current != child_name: 00323 self.view[self.current].deactivate() 00324 if self._get_child_width() > self.min_paned_pos: 00325 self.view[child_name].activate(self.current_artist, self.current_album_title) 00326 else: 00327 self.view[child_name].blank_view() 00328 00329 self.current = child_name 00330 00331 00332 class BaseInfoView(GObject.Object): 00333 def __init__(self, *args, **kwargs): 00334 super(BaseInfoView, self).__init__() 00335 00336 def initialise(self, source, shell, plugin, stack, ds, view_name, view_image): 00337 self.stack = stack 00338 00339 self.webview = ArtistInfoWebView() 00340 self.webview.initialise(source, shell) 00341 00342 self.info_scrolled_window = Gtk.ScrolledWindow() 00343 self.info_scrolled_window.props.hexpand = True 00344 self.info_scrolled_window.props.vexpand = True 00345 self.info_scrolled_window.set_shadow_type(Gtk.ShadowType.IN) 00346 self.info_scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC) 00347 self.info_scrolled_window.add(self.webview) 00348 self.info_scrolled_window.show_all() 00349 self.stack.add_named(self.info_scrolled_window, view_name) 00350 00351 theme = Gtk.IconTheme() 00352 default = theme.get_default() 00353 image_name = 'coverart_browser_' + view_name 00354 width, height = get_stock_size() 00355 pixbuf = create_button_image(plugin, view_image) 00356 default.add_builtin_icon(image_name, width, pixbuf) 00357 00358 self.stack.child_set_property(self.info_scrolled_window, "icon-name", image_name) 00359 00360 self.ds = ds 00361 self.shell = shell 00362 self.plugin = plugin 00363 self.file = "" 00364 self.album_title = None 00365 self.artist = None 00366 self.active = False 00367 00368 plugindir = plugin.plugin_info.get_data_dir() 00369 self.basepath = "file://" + urllib.request.pathname2url(plugindir) 00370 self.link_images = self.basepath + '/img/links/' 00371 00372 self.load_tmpl() 00373 self.connect_signals() 00374 00375 def load_tmpl(self): 00376 pass 00377 00378 def connect_signals(self): 00379 pass 00380 00381 def load_view(self): 00382 print ("load_view") 00383 self.webview.load_string(self.file, 'text/html', 'utf-8', self.basepath) 00384 print ("end load_view") 00385 00386 def blank_view(self): 00387 render_file = self.empty_template.render(stylesheet=self.styles) 00388 self.webview.load_string(render_file, 'text/html', 'utf-8', self.basepath) 00389 00390 def loading(self, current_artist, current_album_title): 00391 pass 00392 00393 def activate(self, artist=None, album_title=None): 00394 print("activating Artist Tab") 00395 self.active = True 00396 self.reload(artist, album_title) 00397 00398 def deactivate(self): 00399 print("deactivating Artist Tab") 00400 self.active = False 00401 00402 00403 class ArtistInfoView(BaseInfoView): 00404 def __init__(self, *args, **kwargs): 00405 super(ArtistInfoView, self).__init__(self, *args, **kwargs) 00406 00407 def initialise(self, source, shell, plugin, stack, ds, link_ds): 00408 super(ArtistInfoView, self).initialise(source, shell, plugin, stack, ds, "artist", "microphone.png") 00409 00410 self.link_ds = link_ds 00411 00412 def loading(self, current_artist, current_album_title): 00413 cl = CoverLocale() 00414 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00415 00416 self.link_ds.set_artist(current_artist) 00417 self.link_ds.set_album(current_album_title) 00418 self.loading_file = self.loading_template.render( 00419 artist=current_artist, 00420 info=_("Loading biography for %s") % current_artist, 00421 song="", 00422 basepath=self.basepath) 00423 self.webview.load_string(self.loading_file, 'text/html', 'utf-8', self.basepath) 00424 00425 def load_tmpl(self): 00426 cl = CoverLocale() 00427 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00428 00429 path = rb.find_plugin_file(self.plugin, 'tmpl/artist-tmpl.html') 00430 empty_path = rb.find_plugin_file(self.plugin, 'tmpl/artist_empty-tmpl.html') 00431 loading_path = rb.find_plugin_file(self.plugin, 'tmpl/loading.html') 00432 self.template = Template(filename=path) 00433 self.loading_template = Template(filename=loading_path) 00434 self.empty_template = Template(filename=empty_path) 00435 self.styles = self.basepath + '/tmpl/artistmain.css' 00436 00437 def connect_signals(self): 00438 self.air_id = self.ds.connect('artist-info-ready', self.artist_info_ready) 00439 00440 def artist_info_ready(self, ds): 00441 # Can only be called after the artist-info-ready signal has fired. 00442 # If called any other time, the behavior is undefined 00443 try: 00444 info = ds.get_artist_info() 00445 00446 small, med, big = info['images'] or (None, None, None) 00447 summary, full_bio = info['bio'] or (None, None) 00448 00449 link_album = self.link_ds.get_album() 00450 if not link_album: 00451 link_album = "" 00452 00453 links = self.link_ds.get_album_links() 00454 if not links: 00455 links = {} 00456 00457 self.file = self.template.render(artist=ds.get_current_artist(), 00458 error=ds.get_error(), 00459 image=med, 00460 fullbio=full_bio, 00461 shortbio=summary, 00462 datasource=lastfm_datasource_link(self.basepath), 00463 stylesheet=self.styles, 00464 album=link_album, 00465 art_links=self.link_ds.get_artist_links(), 00466 alb_links=links, 00467 link_images=self.link_images, 00468 similar=ds.get_similar_info()) 00469 self.load_view() 00470 except Exception as e: 00471 print("Problem in info ready: %s" % e) 00472 00473 00474 def reload(self, artist, album_title): 00475 if not artist: 00476 return 00477 00478 if self.active and artist_exceptions(artist): 00479 print("blank") 00480 self.blank_view() 00481 return 00482 00483 #self.stack.set_visible_child_name(self.view_name) 00484 if self.active and ( (not self.artist or self.artist != artist) 00485 or (not self.album_title or self.album_title != album_title) 00486 ): 00487 print("now loading") 00488 self.loading(artist, album_title) 00489 print("active") 00490 self.ds.fetch_artist_data(artist) 00491 else: 00492 print("load_view") 00493 self.load_view() 00494 00495 self.album_title = album_title 00496 self.artist = artist 00497 00498 00499 class ArtistDataSource(GObject.GObject): 00500 __gsignals__ = { 00501 'artist-info-ready': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) 00502 } 00503 00504 def __init__(self, info_cache, ranking_cache): 00505 GObject.GObject.__init__(self) 00506 00507 self.current_artist = None 00508 self.error = None 00509 #' 'signal' : 'artist-info-ready', ' 00510 self.artist = { 00511 'info': { 00512 'data': None, 00513 'function': 'getinfo', 00514 'cache': info_cache, 00515 'signal': 'artist-info-ready', 00516 'parsed': False 00517 }, 00518 'similar': { 00519 'data': None, 00520 'function': 'getsimilar', 00521 'cache': info_cache, 00522 'signal': 'artist-info-ready', 00523 'parsed': False 00524 } 00525 } 00526 00527 def fetch_artist_data(self, artist): 00528 """ 00529 Initiate the fetching of all artist data. Fetches artist info, similar 00530 artists, artist top albums and top tracks. Downloads XML files from last.fm 00531 and saves as parsed DOM documents in self.artist dictionary. Must be called 00532 before any of the get_* methods. 00533 """ 00534 self.current_artist = artist 00535 if LastFM.user_has_account() is False: 00536 self.error = LASTFM_NO_ACCOUNT_ERROR 00537 self.emit('artist-info-ready') 00538 return 00539 00540 self.error = None 00541 artist = urllib.parse.quote_plus(artist) 00542 self.fetched = 0 00543 for key, value in self.artist.items(): 00544 print("search") 00545 cachekey = "lastfm:artist:%sjson:%s" % (value['function'], artist) 00546 url = '%s?method=artist.%s&artist=%s&limit=10&api_key=%s&format=json' % (LastFM.API_URL, 00547 value['function'], artist, 00548 LastFM.API_KEY) 00549 print("fetching %s" % url) 00550 value['cache'].fetch(cachekey, url, self.fetch_artist_data_cb, value) 00551 00552 def fetch_artist_data_cb(self, data, category): 00553 if data is None: 00554 print("no data fetched for artist %s" % category['function']) 00555 return 00556 00557 print(category) 00558 try: 00559 category['data'] = json.loads(data.decode('utf-8')) 00560 category['parsed'] = False 00561 self.fetched += 1 00562 if self.fetched == len(self.artist): 00563 self.emit(category['signal']) 00564 00565 except Exception as e: 00566 print("Error parsing artist %s: %s" % (category['function'], e)) 00567 return False 00568 00569 def get_current_artist(self): 00570 return self.current_artist 00571 00572 def get_error(self): 00573 return self.error 00574 00575 def get_artist_images(self): 00576 """ 00577 Returns tuple of image url's for small, medium, and large images. 00578 """ 00579 data = self.artist['info']['data'] 00580 if data is None: 00581 return None 00582 00583 images = [img['#text'] for img in data['artist'].get('image', ())] 00584 return images[:3] 00585 00586 def get_artist_bio(self): 00587 """ 00588 Returns tuple of summary and full bio 00589 """ 00590 data = self.artist['info']['data'] 00591 if data is None: 00592 return None 00593 00594 if not self.artist['info']['parsed']: 00595 content = data['artist']['bio']['content'] 00596 summary = data['artist']['bio']['summary'] 00597 return summary, content 00598 00599 return self.artist['info']['data']['bio'] 00600 00601 def get_similar_info(self): 00602 """ 00603 Returns the dictionary { 'images', 'bio' } 00604 """ 00605 if not self.artist['similar']['parsed']: 00606 json_artists_data = self.artist['similar']['data']['similarartists'] 00607 00608 results = [] 00609 for json_artist in json_artists_data["artist"]: 00610 name = json_artist["name"] 00611 image_url = json_artist["image"][1]["#text"] 00612 similarity = int(100 * float(json_artist["match"])) 00613 00614 results.append({'name': name, 00615 'image_url': image_url, 00616 'similarity': similarity}) 00617 00618 self.artist['similar']['data'] = results 00619 self.artist['similar']['parsed'] = True 00620 00621 return self.artist['similar']['data'] 00622 00623 def get_artist_info(self): 00624 """ 00625 Returns the dictionary { 'images', 'bio' } 00626 """ 00627 if not self.artist['info']['parsed']: 00628 images = self.get_artist_images() 00629 bio = self.get_artist_bio() 00630 self.artist['info']['data'] = {'images': images, 00631 'bio': bio} 00632 self.artist['info']['parsed'] = True 00633 00634 return self.artist['info']['data'] 00635 00636 00637 class LinksDataSource(GObject.GObject): 00638 def __init__(self): 00639 GObject.GObject.__init__(self) 00640 print("init") 00641 self.entry = None 00642 self.error = None 00643 00644 self.artist = None 00645 self.album = None 00646 00647 def set_artist(self, artist): 00648 print("set_artist") 00649 self.artist = artist 00650 00651 def get_artist(self): 00652 print("get_artist") 00653 return self.artist 00654 00655 def set_album(self, album): 00656 self.album = album 00657 00658 def get_album(self): 00659 return self.album 00660 00661 def get_artist_links(self): 00662 """ 00663 Return a dictionary with artist URLs to popular music databases and 00664 encyclopedias. 00665 """ 00666 print("get_artist_links") 00667 artist = self.get_artist() 00668 if artist is not "" and artist is not None: 00669 wpartist = artist.replace(" ", "_") 00670 artist = urllib.parse.quote_plus(artist) 00671 artist_links = { 00672 "Wikipedia": "http://www.wikipedia.org/wiki/%s" % wpartist, 00673 "Discogs": "http://www.discogs.com/artist/%s" % artist, 00674 "Allmusic": "http://www.allmusic.com/search/artist/%s" % artist 00675 } 00676 return artist_links 00677 print("no links returned") 00678 print(artist) 00679 00680 return None 00681 00682 def get_album_links(self): 00683 """ 00684 Return a dictionary with album URLs to popular music databases and 00685 encyclopedias. 00686 """ 00687 print("get_album_links") 00688 album = self.get_album() 00689 print(album) 00690 if album is not None and album is not "": 00691 print("obtaining links") 00692 wpalbum = album.replace(" ", "_") 00693 album = urllib.parse.quote_plus(album) 00694 album_links = { 00695 "Wikipedia": "http://www.wikipedia.org/wiki/%s" % wpalbum, 00696 "Discogs": "http://www.discogs.com/search?type=album&q=%s&f=html" % album, 00697 "Allmusic": "http://allmusic.com/search/album/%s" % album 00698 } 00699 return album_links 00700 return None 00701 00702 def get_error(self): 00703 if self.get_artist() is "": 00704 return _("No artist specified.") 00705 00706 00707 class AlbumInfoView(BaseInfoView): 00708 def __init__(self, *args, **kwargs): 00709 super(AlbumInfoView, self).__init__(self, *args, **kwargs) 00710 00711 def initialise(self, source, shell, plugin, stack, ds): 00712 super(AlbumInfoView, self).initialise(source, shell, plugin, stack, ds, "album", "covermgr.png") 00713 00714 def connect_signals(self): 00715 self.ds.connect('albums-ready', self.album_list_ready) 00716 00717 def loading(self, current_artist, current_album_title): 00718 cl = CoverLocale() 00719 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00720 00721 self.loading_file = self.loading_template.render( 00722 artist=current_artist, 00723 # Translators: 'top' here means 'most popular'. %s is replaced by the artist name. 00724 info=_("Loading top albums for %s") % current_artist, 00725 song="", 00726 basepath=self.basepath) 00727 self.webview.load_string(self.loading_file, 'text/html', 'utf-8', self.basepath) 00728 00729 def load_tmpl(self): 00730 cl = CoverLocale() 00731 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00732 00733 path = rb.find_plugin_file(self.plugin, 'tmpl/album-tmpl.html') 00734 empty_path = rb.find_plugin_file(self.plugin, 'tmpl/album_empty-tmpl.html') 00735 self.loading_path = rb.find_plugin_file(self.plugin, 'tmpl/loading.html') 00736 self.album_template = Template(filename=path) 00737 self.loading_template = Template(filename=self.loading_path) 00738 self.empty_template = Template(filename=empty_path) 00739 self.styles = self.basepath + '/tmpl/artistmain.css' 00740 00741 def album_list_ready(self, ds): 00742 print ("album_list_ready") 00743 self.file = self.album_template.render(error=ds.get_error(), 00744 albums=ds.get_top_albums(), 00745 artist=ds.get_artist(), 00746 datasource=lastfm_datasource_link(self.basepath), 00747 stylesheet=self.styles) 00748 self.load_view() 00749 00750 def reload(self, artist, album_title): 00751 print ("reload") 00752 if not artist: 00753 return 00754 00755 if self.active and artist_exceptions(artist): 00756 print("blank") 00757 self.blank_view() 00758 return 00759 00760 if self.active and (not self.artist or artist != self.artist): 00761 self.loading(artist, album_title) 00762 self.ds.fetch_album_list(artist) 00763 else: 00764 self.load_view() 00765 00766 self.album_title = album_title 00767 self.artist = artist 00768 00769 00770 class AlbumDataSource(GObject.GObject): 00771 __gsignals__ = { 00772 'albums-ready': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) 00773 } 00774 00775 def __init__(self, info_cache, ranking_cache): 00776 GObject.GObject.__init__(self) 00777 self.albums = None 00778 self.error = None 00779 self.artist = None 00780 self.max_albums_fetched = 8 00781 self.fetching = 0 00782 self.info_cache = info_cache 00783 self.ranking_cache = ranking_cache 00784 00785 def get_artist(self): 00786 return self.artist 00787 00788 def get_error(self): 00789 return self.error 00790 00791 def fetch_album_list(self, artist): 00792 if LastFM.user_has_account() is False: 00793 self.error = LASTFM_NO_ACCOUNT_ERROR 00794 self.emit('albums-ready') 00795 return 00796 00797 self.artist = artist 00798 qartist = urllib.parse.quote_plus(artist) 00799 self.error = None 00800 url = "%s?method=artist.gettopalbums&artist=%s&api_key=%s&format=json" % ( 00801 LastFM.API_URL, qartist, LastFM.API_KEY) 00802 print(url) 00803 cachekey = 'lastfm:artist:gettopalbumsjson:%s' % qartist 00804 self.ranking_cache.fetch(cachekey, url, self.parse_album_list, artist) 00805 00806 def parse_album_list(self, data, artist): 00807 if data is None: 00808 print("Nothing fetched for %s top albums" % artist) 00809 return False 00810 00811 try: 00812 parsed = json.loads(data.decode("utf-8")) 00813 except Exception as e: 00814 print("Error parsing album list: %s" % e) 00815 return False 00816 00817 self.error = parsed.get('error') 00818 if self.error: 00819 self.emit('albums-ready') 00820 return False 00821 00822 try: 00823 albums = parsed['topalbums'].get('album', [])[:self.max_albums_fetched] 00824 except: 00825 albums = [] 00826 00827 if len(albums) == 0: 00828 self.error = "No albums found for %s" % artist 00829 self.emit('albums-ready') 00830 return True 00831 print(albums) 00832 self.albums = [] 00833 print(len(albums)) 00834 #albums = parsed['topalbums'].get('album', [])[:self.max_albums_fetched] 00835 self.fetching = len(albums) 00836 for i, a in enumerate(albums): 00837 try: 00838 images = [img['#text'] for img in a.get('image', [])] 00839 self.albums.append({'title': a.get('name'), 'images': images[:3]}) 00840 self.fetch_album_info(artist, a.get('name'), i) 00841 except: 00842 pass 00843 00844 return True 00845 00846 def get_top_albums(self): 00847 return self.albums 00848 00849 def fetch_album_info(self, artist, album, index): 00850 qartist = urllib.parse.quote_plus(artist) 00851 qalbum = urllib.parse.quote_plus(album) 00852 cachekey = "lastfm:album:getinfojson:%s:%s" % (qartist, qalbum) 00853 url = "%s?method=album.getinfo&artist=%s&album=%s&api_key=%s&format=json" % ( 00854 LastFM.API_URL, qartist, qalbum, LastFM.API_KEY) 00855 self.info_cache.fetch(cachekey, url, self.parse_album_info, album, index) 00856 00857 def parse_album_info(self, data, album, index): 00858 rv = True 00859 try: 00860 parsed = json.loads(data.decode('utf-8')) 00861 self.albums[index]['id'] = parsed['album']['id'] 00862 00863 for k in ('releasedate', 'summary'): 00864 self.albums[index][k] = parsed['album'].get(k) 00865 00866 tracklist = [] 00867 tracks = parsed['album']['tracks'].get('track', []) 00868 for i, t in enumerate(tracks): 00869 title = t['name'] 00870 duration = int(t['duration']) 00871 tracklist.append((i, title, duration)) 00872 00873 self.albums[index]['tracklist'] = tracklist 00874 self.albums[index]['duration'] = sum([t[2] for t in tracklist]) 00875 00876 if 'wiki' in parsed['album']: 00877 self.albums[index]['wiki-summary'] = parsed['album']['wiki']['summary'] 00878 self.albums[index]['wiki-content'] = parsed['album']['wiki']['content'] 00879 00880 except Exception as e: 00881 print("Error parsing album tracklist: %s" % e) 00882 rv = False 00883 00884 self.fetching -= 1 00885 print("%s albums left to process" % self.fetching) 00886 if self.fetching == 0: 00887 self.emit('albums-ready') 00888 00889 return rv 00890 00891 00892 class EchoArtistInfoView(BaseInfoView): 00893 def __init__(self, *args, **kwargs): 00894 super(EchoArtistInfoView, self).__init__(self, *args, **kwargs) 00895 00896 def initialise(self, source, shell, plugin, stack, ds, link_ds): 00897 super(EchoArtistInfoView, self).initialise(source, shell, plugin, stack, ds, "echoartist", 00898 "echonest_minilogo.gif") 00899 00900 self.link_ds = link_ds 00901 00902 def load_tmpl(self): 00903 cl = CoverLocale() 00904 cl.switch_locale(cl.Locale.LOCALE_DOMAIN) 00905 00906 path = rb.find_plugin_file(self.plugin, 'tmpl/echoartist-tmpl.html') 00907 empty_path = rb.find_plugin_file(self.plugin, 'tmpl/artist_empty-tmpl.html') 00908 loading_path = rb.find_plugin_file(self.plugin, 'tmpl/loading.html') 00909 self.template = Template(filename=path) 00910 self.loading_template = Template(filename=loading_path) 00911 self.empty_template = Template(filename=empty_path) 00912 self.styles = self.basepath + '/tmpl/artistmain.css' 00913 print(lastfm_datasource_link(self.basepath)) 00914 00915 def connect_signals(self): 00916 self.air_id = self.ds.connect('artist-info-ready', self.artist_info_ready) 00917 00918 def artist_info_ready(self, ds): 00919 # Can only be called after the artist-info-ready signal has fired. 00920 # If called any other time, the behavior is undefined 00921 #try: 00922 link_album = self.link_ds.get_album() 00923 if not link_album: 00924 link_album = "" 00925 00926 links = self.link_ds.get_album_links() 00927 if not links: 00928 links = {} 00929 00930 print("#############") 00931 print(ds.get_current_artist()) 00932 print(self.ds.get_artist_bio()) 00933 print(self.styles) 00934 print(self.link_ds.get_artist_links()) 00935 print(links) 00936 print(self.link_images) 00937 print(lastfm_datasource_link(self.basepath)) 00938 print("##############") 00939 self.file = self.template.render(artist=ds.get_current_artist(), 00940 error=ds.get_error(), 00941 bio=self.ds.get_artist_bio(), 00942 stylesheet=self.styles, 00943 album=link_album, 00944 art_links=self.link_ds.get_artist_links(), 00945 alb_links=links, 00946 link_images=self.link_images, 00947 datasource=ds.get_attribution()) 00948 self.load_view() 00949 #except Exception as e: 00950 # print("Problem in info ready: %s" % e) 00951 00952 def reload(self, artist, album_title): 00953 if not artist: 00954 return 00955 00956 if self.active and artist_exceptions(artist): 00957 print("blank") 00958 self.blank_view() 00959 return 00960 00961 #self.stack.set_visible_child_name(self.view_name) 00962 if self.active and ( (not self.artist or self.artist != artist) 00963 or (not self.album_title or self.album_title != album_title) 00964 ): 00965 print("now loading") 00966 self.loading(artist, album_title) 00967 print("active") 00968 self.ds.fetch_artist_data(artist) 00969 else: 00970 print("load_view") 00971 self.load_view() 00972 00973 self.album_title = album_title 00974 self.artist = artist 00975 00976 00977 class EchoArtistDataSource(GObject.GObject): 00978 __gsignals__ = { 00979 'artist-info-ready': (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ()) 00980 } 00981 00982 def __init__(self, info_cache, ranking_cache): 00983 GObject.GObject.__init__(self) 00984 00985 self.current_artist = None 00986 self.error = None 00987 self.artist = { 00988 'info': { 00989 'data': None, 00990 'cache': info_cache, 00991 'signal': 'artist-info-ready', 00992 'parsed': False 00993 } 00994 } 00995 00996 def fetch_artist_data(self, artist): 00997 """ 00998 Initiate the fetching of all artist data. Fetches artist info, similar 00999 artists, artist top albums and top tracks. Downloads XML files from last.fm 01000 and saves as parsed DOM documents in self.artist dictionary. Must be called 01001 before any of the get_* methods. 01002 """ 01003 self.current_artist = artist 01004 01005 self.error = None 01006 artist = urllib.parse.quote_plus(artist) 01007 self.fetched = 0 01008 for key, value in self.artist.items(): 01009 print("search") 01010 cachekey = "echonest:artist:json:%s" % (artist) 01011 api_url = "http://developer.echonest.com/api/v4/" 01012 api_key = "N685TONJGZSHBDZMP" 01013 url = '%sartist/biographies?api_key=%s&name=%s&format=json&results=1&start=0' % (api_url, 01014 api_key, artist) 01015 01016 #http://developer.echonest.com/api/v4/artist/biographies?api_key=N685TONJGZSHBDZMP&name=queen&format=json&results=1&start=0 01017 #http://developer.echonest.com/api/v4/artist/biographies?api_key=N685TONJGZSHBDZMP?name=ABBA&format=json&results=1&start=0 01018 print("fetching %s" % url) 01019 value['cache'].fetch(cachekey, url, self.fetch_artist_data_cb, value) 01020 01021 def fetch_artist_data_cb(self, data, category): 01022 if data is None: 01023 print("no data fetched for artist") 01024 return 01025 01026 print(category) 01027 try: 01028 category['data'] = json.loads(data.decode('utf-8')) 01029 category['parsed'] = False 01030 self.fetched += 1 01031 if self.fetched == len(self.artist): 01032 self.emit(category['signal']) 01033 01034 except Exception as e: 01035 print("Error parsing artist") 01036 return False 01037 01038 def get_current_artist(self): 01039 return self.current_artist 01040 01041 def get_error(self): 01042 return self.error 01043 01044 def get_attribution(self): 01045 data = self.artist['info']['data'] 01046 if data is None: 01047 return None 01048 01049 content = "" 01050 01051 if not self.artist['info']['parsed']: 01052 print(data) 01053 url = data['response']['biographies'][0]['url'] 01054 site = data['response']['biographies'][0]['site'] 01055 print(url) 01056 print(site) 01057 return "<a href='%s'>%s</a>" % (url, site) 01058 01059 return content 01060 01061 def get_artist_bio(self): 01062 """ 01063 Returns tuple of summary and full bio 01064 """ 01065 data = self.artist['info']['data'] 01066 if data is None: 01067 return None 01068 01069 if not self.artist['info']['parsed']: 01070 print(data) 01071 content = data['response']['biographies'][0]['text'] 01072 return content 01073 01074 return self.artist['info']['data']['response']['biographies'][0]['text']