add artist-info-pane implementation - issue #268

Esse commit está contido em:
fossfreedom
2014-02-04 00:03:01 +00:00
commit 0b97a39edc
19 arquivos alterados com 1149 adições e 20 exclusões
+793
Ver Arquivo
@@ -0,0 +1,793 @@
# -*- Mode: python; coding: utf-8; tab-width: 4; indent-tabs-mode: nil; -*-
#
# Copyright (C) 2014 fossfreedom
# this module has been heavily modifed from rhythmbox context plugin
# Copyright (C) 2009 John Iacona
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2, or (at your option)
# any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
import re, os
import cgi
import urllib.request, urllib.parse
import json
from mako.template import Template
import rb
import rb_lastfm as LastFM# from coverart-search-providers
from gi.repository import WebKit
from gi.repository import GObject
from gi.repository import Gtk
from gi.repository import Gdk
from gi.repository import GLib
from gi.repository import RB
from gi.repository import Gio
from coverart_utils import create_pixbuf_from_file_at_size
from coverart_utils import get_stock_size
from coverart_widgets import PixbufButton
from coverart_browser_prefs import GSetting
import gettext
gettext.install('rhythmbox', RB.locale_dir())
def artist_exceptions(artist):
exceptions = ['various']
for exception in exceptions:
if exception in artist.lower():
return True
return False
def lastfm_datasource_link(path):
return "<a href='http://last.fm/'><img src='%s/img/lastfm.png'></a>" % path
LASTFM_NO_ACCOUNT_ERROR = _("This information is only available to Last.fm users. Ensure the Last.fm plugin is enabled, select Last.fm in the side pane, and log in.")
def create_button_image(plugin, icon_name):
path = 'img/'
return create_pixbuf_from_file_at_size(
rb.find_plugin_file(plugin, path + icon_name),
*get_stock_size())
class ArtistInfoPane(GObject.GObject):
__gsignals__ = {
'selected' : (GObject.SIGNAL_RUN_LAST, None,
(GObject.TYPE_STRING,GObject.TYPE_STRING))
}
artist_info_paned_pos = GObject.property(type=str)
def __init__(self, button_box, scroll_window, info_paned, source):
GObject.GObject.__init__ (self)
self.tab = {}
self.ds = {}
self.view = {}
self.buttons = button_box
self.source = source
self.plugin = source.plugin
self.shell = source.shell
self.info_scrolled_window = scroll_window
self.info_paned = info_paned
self.current_artist = None
self.current_album_title = None
self.webview = WebKit.WebView()
self.webview.connect("navigation-requested", self.navigation_request_cb)
self.info_scrolled_window.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.AUTOMATIC)
self.info_scrolled_window.add (self.webview)
self.info_scrolled_window.show_all()
# cache for artist/album information: valid for a month, can be used indefinitely
# if offline, discarded if unused for six months
self.info_cache = rb.URLCache(name = 'info',
path = os.path.join('coverart_browser', 'info'),
refresh = 30,
discard = 180)
# cache for rankings (artist top tracks and top albums): valid for a week,
# can be used for a month if offline
self.ranking_cache = rb.URLCache(name = 'ranking',
path = os.path.join('coverart_browser', 'ranking'),
refresh = 7,
lifetime = 30)
self.info_cache.clean()
self.ranking_cache.clean()
self.ds['link'] = LinksDataSource ()
self.ds['artist'] = ArtistDataSource (self.info_cache, self.ranking_cache)
self.view['artist'] = ArtistInfoView (self.shell, self.plugin, self.webview, self.ds['artist'], self.ds['link'])
self.tab['artist'] = ArtistInfoTab (self.plugin, self.shell, self.buttons, self.ds['artist'], self.view['artist'])
self.ds['album'] = AlbumDataSource(self.info_cache, self.ranking_cache)
self.view['album'] = AlbumInfoView(self.shell, self.plugin, self.webview, self.ds['album'])
self.tab['album'] = AlbumInfoTab(self.plugin, self.shell, self.buttons, self.ds['album'], self.view['album'])
self.gs = GSetting()
self.connect_properties()
self.connect_signals()
Gdk.threads_add_timeout(GLib.PRIORITY_DEFAULT_IDLE, 50, self._change_paned_pos, self.source.viewmgr.view_name)
self.current = 'artist'
self.tab[self.current].activate ()
def connect_properties(self):
'''
Connects the source properties to the saved preferences.
'''
setting = self.gs.get_setting(self.gs.Path.PLUGIN)
setting.bind(
self.gs.PluginKey.ARTIST_INFO_PANED_POSITION,
self,
'artist-info-paned-pos',
Gio.SettingsBindFlags.DEFAULT)
def connect_signals(self):
self.tab_cb_ids = []
# Listen for switch-tab signal from each tab
for key, value in self.tab.items():
self.tab_cb_ids.append((key, self.tab[key].connect ('switch-tab', self.change_tab)))
# Listen for selected signal from the views
self.connect('selected', self.select_artist)
# lets remember info paned click
self.info_paned.connect('button-release-event',
self.artist_info_paned_button_release_callback)
# lets also listen for changes to the view to set the paned position
self.source.viewmgr.connect('new-view', self.on_view_changed)
def on_view_changed(self, widget, view_name):
self._change_paned_pos(view_name)
def _change_paned_pos(self, view_name):
paned_positions = eval(self.artist_info_paned_pos)
found = None
for viewpos in paned_positions:
if view_name in viewpos:
found = viewpos
break
if not found:
return
child_width = int(found.split(":")[1])
calc_pos = self.source.page.get_allocated_width() - child_width
self.info_paned.set_position(calc_pos)
self.info_paned.set_visible(True)
def artist_info_paned_button_release_callback(self, *args):
'''
Callback when the artist paned handle is released from its mouse click.
'''
child = self.info_paned.get_child2()
child_width = child.get_allocated_width()
paned_positions = eval(self.artist_info_paned_pos)
found = None
for viewpos in paned_positions:
if self.source.viewmgr.view_name in viewpos:
found = viewpos
break
if not found:
return
paned_positions.remove(found)
paned_positions.append(self.source.viewmgr.view_name + ":" + str(child_width))
self.artist_info_paned_pos = repr(paned_positions)
def select_artist(self, widget, artist, album_title):
self.tab[self.current].reload(artist, album_title)
self.current_album_title = album_title
self.current_artist = artist
def change_tab (self, tab, newtab):
print("swapping tab from %s to %s" % (self.current, newtab))
if (self.current != newtab):
self.tab[self.current].deactivate()
self.tab[newtab].activate(self.current_artist, self.current_album_title)
self.current = newtab
def navigation_request_cb(self, view, frame, request):
# open HTTP URIs externally. this isn't a web browser.
if request.get_uri().startswith('http'):
print("opening uri %s" % request.get_uri())
Gtk.show_uri(self.shell.props.window.get_screen(), request.get_uri(), Gdk.CURRENT_TIME)
return 1 # WEBKIT_NAVIGATION_RESPONSE_IGNORE
else:
return 0 # WEBKIT_NAVIGATION_RESPONSE_ACCEPT
class ArtistInfoTab (GObject.GObject):
__gsignals__ = {
'switch-tab' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
(GObject.TYPE_STRING,))
}
def __init__ (self, plugin, shell, buttons, ds, view):
GObject.GObject.__init__ (self)
self.shell = shell
self.sp = shell.props.shell_player
self.db = shell.props.db
self.buttons = buttons
self.button = PixbufButton()#Gtk.ToggleButton (label=_("Artist"))
self.button.set_image(create_button_image(plugin, "microphone.png"))
self.datasource = ds
self.view = view
self.album_title= None
self.artist = None
self.active = False
self.button.show()
self.button.set_relief (Gtk.ReliefStyle.NONE)
self.button.set_focus_on_click(False)
self.button.connect ('clicked',
lambda button : self.emit('switch-tab', 'artist'))
buttons.pack_start (self.button, False, True, 0)
def activate (self, artist=None, album_title=None):
print("activating Artist Tab")
self.button.set_active(True)
self.active = True
self.reload (artist, album_title)
def deactivate (self):
print("deactivating Artist Tab")
self.button.set_active(False)
self.active = False
def reload (self, artist, album_title):
if not artist:
return
if self.active and artist_exceptions(artist):
print ("blank")
self.view.blank_view()
return
if self.active and ( (not self.artist or self.artist != artist)
or (not self.album_title or self.album_title != album_title)
):
print ("now loading")
self.view.loading (artist, album_title)
print ("active")
self.datasource.fetch_artist_data (artist)
else:
print ("load_view")
self.view.load_view()
self.album_title = album_title
self.artist = artist
class ArtistInfoView (GObject.GObject):
def __init__ (self, shell, plugin, webview, ds, link_ds):
GObject.GObject.__init__ (self)
self.webview = webview
self.ds = ds
self.link_ds = link_ds
self.shell = shell
self.plugin = plugin
self.file = ""
plugindir = plugin.plugin_info.get_data_dir()
self.basepath = "file://" + urllib.request.pathname2url (plugindir)
self.link_images = self.basepath + '/img/links/'
self.load_tmpl ()
self.connect_signals ()
def load_view (self):
self.webview.load_string (self.file, 'text/html', 'utf-8', self.basepath)
def blank_view (self):
render_file = self.empty_template.render( stylesheet = self.styles )
self.webview.load_string (render_file, 'text/html', 'utf-8', self.basepath)
def loading (self, current_artist, current_album_title):
self.link_ds.set_artist (current_artist)
self.link_ds.set_album (current_album_title)
self.loading_file = self.loading_template.render (
artist = current_artist,
info = _("Loading biography for %s") % current_artist,
song = "",
basepath = self.basepath)
self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
def load_tmpl (self):
path = rb.find_plugin_file(self.plugin, 'tmpl/artist-tmpl.html')
empty_path = rb.find_plugin_file(self.plugin, 'tmpl/artist_empty-tmpl.html')
loading_path = rb.find_plugin_file (self.plugin, 'tmpl/loading.html')
self.template = Template (filename = path)
self.loading_template = Template (filename = loading_path)
self.empty_template = Template (filename = empty_path)
self.styles = self.basepath + '/tmpl/artistmain.css'
def connect_signals (self):
self.air_id = self.ds.connect ('artist-info-ready', self.artist_info_ready)
def artist_info_ready (self, ds):
# Can only be called after the artist-info-ready signal has fired.
# If called any other time, the behavior is undefined
try:
info = ds.get_artist_info ()
small, med, big = info['images'] or (None, None, None)
summary, full_bio = info['bio'] or (None, None)
link_album = self.link_ds.get_album()
if not link_album:
link_album = ""
links = self.link_ds.get_album_links()
if not links:
links = {}
self.file = self.template.render (artist = ds.get_current_artist (),
error = ds.get_error (),
image = med,
fullbio = full_bio,
shortbio = summary,
datasource = lastfm_datasource_link (self.basepath),
stylesheet = self.styles,
album = link_album,
art_links = self.link_ds.get_artist_links (),
alb_links = links,
link_images= self.link_images,
similar = ds.get_similar_info() )
self.load_view ()
except Exception as e:
print("Problem in info ready: %s" % e)
class ArtistDataSource (GObject.GObject):
__gsignals__ = {
'artist-info-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
}
def __init__ (self, info_cache, ranking_cache):
GObject.GObject.__init__ (self)
self.current_artist = None
self.error = None
#' 'signal' : 'artist-info-ready', '
self.artist = {
'info' : {
'data' : None,
'function' : 'getinfo',
'cache' : info_cache,
'signal' : 'artist-info-ready',
'parsed' : False
},
'similar' : {
'data' : None,
'function' : 'getsimilar',
'cache' : info_cache,
'signal' : 'artist-info-ready',
'parsed' : False
}
}
def fetch_artist_data (self, artist):
"""
Initiate the fetching of all artist data. Fetches artist info, similar
artists, artist top albums and top tracks. Downloads XML files from last.fm
and saves as parsed DOM documents in self.artist dictionary. Must be called
before any of the get_* methods.
"""
self.current_artist = artist
if LastFM.user_has_account() is False:
self.error = LASTFM_NO_ACCOUNT_ERROR
self.emit ('artist-info-ready')
return
self.error = None
artist = urllib.parse.quote_plus(artist)
self.fetched = 0
for key, value in self.artist.items():
print ("search")
cachekey = "lastfm:artist:%sjson:%s" % (value['function'], artist)
url = '%s?method=artist.%s&artist=%s&limit=10&api_key=%s&format=json' % (LastFM.API_URL,
value['function'], artist, LastFM.API_KEY)
print("fetching %s" % url)
value['cache'].fetch(cachekey, url, self.fetch_artist_data_cb, value)
def fetch_artist_data_cb (self, data, category):
if data is None:
print("no data fetched for artist %s" % category['function'])
return
print (category)
try:
category['data'] = json.loads(data.decode('utf-8'))
category['parsed'] = False
self.fetched += 1
if self.fetched == len(self.artist):
self.emit (category['signal'])
except Exception as e:
print("Error parsing artist %s: %s" % (category['function'], e))
return False
def get_current_artist (self):
return self.current_artist
def get_error (self):
return self.error
def get_artist_images (self):
"""
Returns tuple of image url's for small, medium, and large images.
"""
data = self.artist['info']['data']
if data is None:
return None
images = [img['#text'] for img in data['artist'].get('image', ())]
return images[:3]
def get_artist_bio (self):
"""
Returns tuple of summary and full bio
"""
data = self.artist['info']['data']
if data is None:
return None
if not self.artist['info']['parsed']:
content = data['artist']['bio']['content']
summary = data['artist']['bio']['summary']
return summary, content
return self.artist['info']['data']['bio']
def get_similar_info(self):
"""
Returns the dictionary { 'images', 'bio' }
"""
if not self.artist['similar']['parsed']:
json_artists_data = self.artist['similar']['data']['similarartists']
results = []
for json_artist in json_artists_data["artist"]:
name = json_artist["name"]
image_url = json_artist["image"][1]["#text"]
similarity = int(100 * float(json_artist["match"]))
results.append( {'name': name,
'image_url': image_url,
'similarity': similarity})
self.artist['similar']['data'] = results
self.artist['similar']['parsed'] = True
return self.artist['similar']['data']
def get_artist_info (self):
"""
Returns the dictionary { 'images', 'bio' }
"""
if not self.artist['info']['parsed']:
images = self.get_artist_images()
bio = self.get_artist_bio()
self.artist['info']['data'] = { 'images' : images,
'bio' : bio }
self.artist['info']['parsed'] = True
return self.artist['info']['data']
class LinksDataSource (GObject.GObject):
def __init__ (self):
GObject.GObject.__init__ (self)
print ("init")
self.entry = None
self.error = None
self.artist = None
self.album = None
def set_artist (self, artist):
print ("set_artist")
self.artist = artist
def get_artist (self):
print ("get_artist")
return self.artist
def set_album (self, album):
self.album = album
def get_album (self):
return self.album
def get_artist_links (self):
"""
Return a dictionary with artist URLs to popular music databases and
encyclopedias.
"""
print ("get_artist_links")
artist = self.get_artist()
if artist is not "" and artist is not None:
wpartist = artist.replace(" ", "_")
artist = urllib.parse.quote_plus(artist)
artist_links = {
"Wikipedia" : "http://www.wikipedia.org/wiki/%s" % wpartist,
"Discogs" : "http://www.discogs.com/artist/%s" % artist,
"Allmusic" : "http://www.allmusic.com/search/artist/%s" % artist
}
return artist_links
return None
def get_album_links (self):
"""
Return a dictionary with album URLs to popular music databases and
encyclopedias.
"""
print ("get_album_links")
album = self.get_album()
print (album)
if album is not None and album is not "":
print ("obtaining links")
wpalbum = album.replace(" ", "_")
album = urllib.parse.quote_plus(album)
album_links = {
"Wikipedia" : "http://www.wikipedia.org/wiki/%s" % wpalbum,
"Discogs" : "http://www.discogs.com/search?type=album&q=%s&f=html" % album,
"Allmusic" : "http://allmusic.com/search/album/%s" % album
}
return album_links
return None
def get_error (self):
if self.get_artist() is "":
return _("No artist specified.")
class AlbumInfoTab (GObject.GObject):
__gsignals__ = {
'switch-tab' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE,
(GObject.TYPE_STRING,))
}
def __init__ (self, plugin, shell, buttons, ds, view):
GObject.GObject.__init__ (self)
self.shell = shell
self.sp = shell.props.shell_player
self.db = shell.props.db
self.buttons = buttons
#self.button = Gtk.ToggleButton (label=_("Albums"))
self.button = PixbufButton()
self.button.set_image(create_button_image(plugin, "covermgr.png"))
self.ds = ds
self.view = view
self.album_title= None
self.artist = None
self.active = False
self.button.show()
self.button.set_relief (Gtk.ReliefStyle.NONE)
self.button.set_focus_on_click(False)
self.button.connect ('clicked',
lambda button: self.emit ('switch-tab', 'album'))
buttons.pack_start (self.button, False, True, 0)
def activate (self, artist, album_title):
self.button.set_active(True)
self.active = True
self.reload (artist, album_title)
def deactivate (self):
self.button.set_active(False)
self.active = False
def reload (self, artist, album_title):
if not artist:
return
if self.active and artist_exceptions(artist):
print ("blank")
self.view.blank_view()
return
if self.active and (not self.artist or artist != self.artist):
self.view.loading(artist, album_title)
self.ds.fetch_album_list (artist)
else:
self.view.load_view()
self.album_title = album_title
self.artist = artist
class AlbumInfoView (GObject.GObject):
def __init__ (self, shell, plugin, webview, ds):
GObject.GObject.__init__ (self)
self.webview = webview
self.ds = ds
self.shell = shell
self.plugin = plugin
self.file = ""
plugindir = plugin.plugin_info.get_data_dir()
self.basepath = "file://" + urllib.request.pathname2url (plugindir)
self.load_tmpl ()
self.connect_signals ()
def load_view (self):
self.webview.load_string(self.file, 'text/html', 'utf-8', self.basepath)
def blank_view (self):
render_file = self.empty_template.render( stylesheet = self.styles )
self.webview.load_string (render_file, 'text/html', 'utf-8', self.basepath)
def connect_signals (self):
self.ds.connect('albums-ready', self.album_list_ready)
def loading (self, current_artist, current_album_title):
self.loading_file = self.loading_template.render (
artist = current_artist,
# Translators: 'top' here means 'most popular'. %s is replaced by the artist name.
info = _("Loading top albums for %s") % current_artist,
song = "",
basepath = self.basepath)
self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
def load_tmpl (self):
path = rb.find_plugin_file (self.plugin, 'tmpl/album-tmpl.html')
empty_path = rb.find_plugin_file (self.plugin, 'tmpl/album_empty-tmpl.html')
self.loading_path = rb.find_plugin_file (self.plugin, 'tmpl/loading.html')
self.album_template = Template (filename = path)
self.loading_template = Template (filename = self.loading_path)
self.empty_template = Template (filename = empty_path)
self.styles = self.basepath + '/tmpl/artistmain.css'
def album_list_ready (self, ds):
self.file = self.album_template.render (error = ds.get_error(),
albums = ds.get_top_albums(),
artist = ds.get_artist(),
datasource = lastfm_datasource_link (self.basepath),
stylesheet = self.styles)
self.load_view ()
class AlbumDataSource (GObject.GObject):
__gsignals__ = {
'albums-ready' : (GObject.SIGNAL_RUN_LAST, GObject.TYPE_NONE, ())
}
def __init__ (self, info_cache, ranking_cache):
GObject.GObject.__init__ (self)
self.albums = None
self.error = None
self.artist = None
self.max_albums_fetched = 8
self.fetching = 0
self.info_cache = info_cache
self.ranking_cache = ranking_cache
def get_artist (self):
return self.artist
def get_error (self):
return self.error
def fetch_album_list (self, artist):
if LastFM.user_has_account() is False:
self.error = LASTFM_NO_ACCOUNT_ERROR
self.emit ('albums-ready')
return
self.artist = artist
qartist = urllib.parse.quote_plus(artist)
self.error = None
url = "%s?method=artist.gettopalbums&artist=%s&api_key=%s&format=json" % (
LastFM.API_URL, qartist, LastFM.API_KEY)
print (url)
cachekey = 'lastfm:artist:gettopalbumsjson:%s' % qartist
self.ranking_cache.fetch(cachekey, url, self.parse_album_list, artist)
def parse_album_list (self, data, artist):
if data is None:
print("Nothing fetched for %s top albums" % artist)
return False
try:
parsed = json.loads(data.decode("utf-8"))
except Exception as e:
print("Error parsing album list: %s" % e)
return False
self.error = parsed.get('error')
if self.error:
self.emit ('albums-ready')
return False
albums = parsed['topalbums'].get('album', [])
if len(albums) == 0:
self.error = "No albums found for %s" % artist
self.emit('albums-ready')
return True
self.albums = []
albums = parsed['topalbums'].get('album', [])[:self.max_albums_fetched]
self.fetching = len(albums)
for i, a in enumerate(albums):
images = [img['#text'] for img in a.get('image', [])]
self.albums.append({'title': a.get('name'), 'images': images[:3]})
self.fetch_album_info(artist, a.get('name'), i)
return True
def get_top_albums (self):
return self.albums
def fetch_album_info (self, artist, album, index):
qartist = urllib.parse.quote_plus(artist)
qalbum = urllib.parse.quote_plus(album)
cachekey = "lastfm:album:getinfojson:%s:%s" % (qartist, qalbum)
url = "%s?method=album.getinfo&artist=%s&album=%s&api_key=%s&format=json" % (
LastFM.API_URL, qartist, qalbum, LastFM.API_KEY)
self.info_cache.fetch(cachekey, url, self.parse_album_info, album, index)
def parse_album_info (self, data, album, index):
rv = True
try:
parsed = json.loads(data.decode('utf-8'))
self.albums[index]['id'] = parsed['album']['id']
for k in ('releasedate', 'summary'):
self.albums[index][k] = parsed['album'].get(k)
tracklist = []
tracks = parsed['album']['tracks'].get('track', [])
for i, t in enumerate(tracks):
title = t['name']
duration = int(t['duration'])
tracklist.append((i, title, duration))
self.albums[index]['tracklist'] = tracklist
self.albums[index]['duration'] = sum([t[2] for t in tracklist])
if 'wiki' in parsed['album']:
self.albums[index]['wiki-summary'] = parsed['album']['wiki']['summary']
self.albums[index]['wiki-content'] = parsed['album']['wiki']['content']
except Exception as e:
print("Error parsing album tracklist: %s" % e)
rv = False
self.fetching -= 1
print("%s albums left to process" % self.fetching)
if self.fetching == 0:
self.emit('albums-ready')
return rv
+1
Ver Arquivo
@@ -958,6 +958,7 @@ class ArtistView(Gtk.TreeView, AbstractView):
active_object = self.artist_manager.model.get_from_path(treepath)
if not isinstance(active_object, Album):
self.source.artist_info.emit('selected', active_object.name, None)
if self.icon_automatic:
# reset counter so that we get correct double click action for albums
self.source.click_count = 0
+2 -1
Ver Arquivo
@@ -164,7 +164,8 @@ class GSetting:
WEBKIT='webkit-support',
ARTIST_PANED_POSITION='artist-paned-pos',
ARTIST_PANED_DISPLAY='artist-paned-display',
USE_FAVOURITES='use-favourites')
USE_FAVOURITES='use-favourites',
ARTIST_INFO_PANED_POSITION='artist-info-paned-pos')
self.setting = {}
+18 -3
Ver Arquivo
@@ -25,6 +25,7 @@ from gi.repository import Gio
from gi.repository import Gdk
from gi.repository import Gtk
from gi.repository import RB
from gi.repository import WebKit
from coverart_album import AlbumManager
from coverart_entryview import CoverArtEntryView as EV
@@ -44,6 +45,7 @@ from coverart_coverflowview import CoverFlowView
from coverart_artistview import ArtistView
from coverart_listview import ListView
from coverart_toolbar import ToolbarManager
from coverart_artistinfo import ArtistInfoPane
import coverart_rb3compat as rb3compat
import random
@@ -230,6 +232,16 @@ class CoverArtBrowserSource(RB.Source):
self.request_cancel_button = ui.get_object('request_cancel_button')
self.paned = ui.get_object('paned')
self.notebook = ui.get_object('bottom_notebook')
#---- set up info pane -----#
info_scrolled_window = ui.get_object('info_scrolled_window')
info_button_box = ui.get_object('info_button_box')
artist_info_paned = ui.get_object('vertical_info_paned')
self.artist_info = ArtistInfoPane(info_button_box,
info_scrolled_window,
artist_info_paned,
self)
# quick search
self.quick_search = ui.get_object('quick_search_entry')
@@ -338,7 +350,7 @@ class CoverArtBrowserSource(RB.Source):
Callback when the artist paned handle is released from its mouse click.
'''
self.artist_paned_pos = self.artist_paned.get_position()
def display_quick_artist_filter_callback(self):
if self.artist_treeview.get_visible():
self.artist_treeview.set_visible(False)
@@ -786,6 +798,9 @@ class CoverArtBrowserSource(RB.Source):
else:
self.stars.set_rating(0)
if len(selected) == 1:
self.artist_info.emit('selected', selected[0].artist, selected[0].name)
for album in selected:
# add the album to the entry_view
self.entry_view.add_album(album)
@@ -1045,7 +1060,7 @@ class Views:
class ViewManager(GObject.Object):
# signals
__gsignals__ = {
'new-view': (GObject.SIGNAL_RUN_LAST, None, ())
'new-view': (GObject.SIGNAL_RUN_LAST, None, (str,))
}
# properties
@@ -1136,7 +1151,7 @@ class ViewManager(GObject.Object):
setting = gs.get_setting(gs.Path.PLUGIN)
setting[gs.PluginKey.VIEW_NAME] = saved_view
self.emit('new-view')
self.emit('new-view', self.view_name)
def get_view_icon_name(self, view_name):
return self._views[view_name].get_view_icon_name()
+1 -1
Ver Arquivo
@@ -248,7 +248,7 @@ class PixbufButton(EnhancedButton):
image = Gtk.Image()
super(PixbufButton, self).set_image(image)
if not self.controller.enabled:
if hasattr(self, "controller.enabled") and not self.controller.enabled:
pixbuf = self._getBlendedPixbuf(pixbuf)
self.get_image().set_from_pixbuf(pixbuf)
BIN
Ver Arquivo
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 1.9 KiB

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 803 B

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 771 B

Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 381 B

BIN
Ver Arquivo
Arquivo binário não exibido.

Depois

Largura:  |  Altura:  |  Tamanho: 4.9 KiB

@@ -206,6 +206,11 @@
<summary>Display the artist filter pane</summary>
<description>Display the artist filter pane</description>
</key>
<key type="s" name="artist-info-paned-pos">
<default>"['coverview:0','artistview:250','coverflowview:0']"</default>
<summary>Position of the artist info paned.</summary>
<description>Position of the artist info paned.</description>
</key>
<key type="b" name="use-favourites">
<default>false</default>
<summary>Use favourites</summary>
+118
Ver Arquivo
@@ -0,0 +1,118 @@
<%page args="error, albums, artist, stylesheet, datasource" />
<html> <head> <meta http-equiv="content-type" content="text-html; charset=utf-8">
<%!
import re
import cgi
import email.utils
from gettext import ngettext
def cleanup(text):
return re.sub(r'\([^\)]*\)', '', text)
def sec2hms(time):
hr = int(time / 3600)
if hr > 0:
time %= 3600
mn = time / 60
sec = time % 60
if hr > 0:
return _("%d:%02d:%02d") % (hr,mn,sec)
else:
return _("%d:%02d") %(mn,sec)
def format_year(date):
try:
parsed = email.utils.parsedate(date)
except Exception as e:
return ""
if parsed is None:
return ""
else:
return '[' + str(parsed[0]) + ']'
%>
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
<script language="javascript">
function swapClass (element, klass1, klass2) {
elt = document.getElementById(element);
elt.className = (elt.className == klass1) ? klass2 : klass1;
}
function swapText (element, text1, text2) {
elt = document.getElementById(element);
elt.innerHTML = (elt.innerHTML == text1) ? text2 : text1;
}
function toggle_vis (element) {
swapClass(element, 'hidden', 'shown');
hide = ${ '"' + _("Hide all tracks") + '"' };
show = ${ '"' + _("Show all tracks") + '"' };
swapText('btn_'+element, hide, show);
}
</script>
<style type="text/css">
.wiki
{ font-size: 10pt;
font-family: sans-serif;
}
</style>
</head>
<body>
%if error is None:
<%
num_albums = len(albums)
%>
<h1>${ _("Top albums by %s") % ("<em>" + cgi.escape(artist, True) + "</em>") }</h1>
%for i, entry in enumerate(albums) :
<%
if 'tracklist' not in entry or len(entry['tracklist']) == 0:
continue
%>
<div id="album${entry['id'] | h}" class="album">
<img width="64" src="${entry['images'][1] | h}" alt="${entry['images'] | h}"/>
<h2>${entry['title'] | h}
%if 'releasedate' in entry:
${ format_year(entry['releasedate']) }
%endif
</h2>
%if 'duration' in entry:
<%
album_time = sec2hms(entry['duration'])
tracks = len(entry['tracklist'])
s = ngettext("%s (%d track)", "%s (%d tracks)", tracks)
%>
<p class="duration">${ s % (album_time, tracks) }</p>
%endif
%if 'tracklist' in entry:
<button id="btn_${entry['id'] | h}" onclick="toggle_vis(${entry['id'] | h})">
${ _("Show all tracks") }
</button>
<table class="hidden" id="${entry['id'] | h}">
%for num, title, time in entry['tracklist'] :
<%
time = sec2hms(time)
title = cleanup(title)
num = num+1
%>
<tr><td>${num}</td><td>${title | h}</td><td>${time}</td></tr>
%endfor
</table>
%else:
<p>${ _("Track list not available") }</p>
%endif
<div class="wiki">
%if 'wiki-summary' in entry:
${entry['wiki-summary']}
%endif
</div>
</div>
%endfor
<p>${datasource}</p>
%else:
<h1>${ _("Unable to retrieve album information:") }</h1>
<p class="error">${error | h}</p>
%endif
</body>
</html>
+9
Ver Arquivo
@@ -0,0 +1,9 @@
<%page args="stylesheet"/>
<html>
<head>
<meta http-equiv="content-type" content="text-html; charset=utf-8">
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
</head>
<body>
</body>
</html>
+79
Ver Arquivo
@@ -0,0 +1,79 @@
<%page args="artist, image, shortbio, fullbio, stylesheet, datasource, album, art_links, alb_links, link_images, similar" />
<%!
import re
import cgi
remove_links = re.compile ('</?a[^>]*> ',re.VERBOSE)
def cleanup(text):
if text is None:
return _("No information available")
text = remove_links.sub ('', text)
text = text.replace('\n', '</p><p>')
return text
%>
<html>
<head>
<meta http-equiv="content-type" content="text-html; charset=utf-8">
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
<style type="text/css">
.shown p,
.similar td,
.links a
{ font-size: 10pt;
font-family: sans-serif;
}
.links img
{ padding: 0 5px 0 5px }
.links ul
{ list-style-type: none }
</style>
</head>
<body class="artist">
%if error is None:
<h1>${artist | h}</h1>
<img src="${image | h}" />
<div id="shortbio" class="shown">
<% shortbio = cleanup(shortbio) %>
<p>${shortbio}</p>
</div>
<div class="links">
<h1>${ _("Links for %s:") % ("<em>" + artist + "</em>")}</h1>
<ul>
%for k, v in art_links.items() :
<li><img src="${link_images}${k}16x16.png" /><a href="${v}">${k}</a></li>
%endfor
</ul>
%if len(alb_links) > 0:
<h1>${ _("Links for %s:") % ("<em>" + album + "</em>")}</h1>
%endif
<ul>
%for k, v in alb_links.items() :
<li><img src="${link_images}${k}16x16.png" /><a href="${v}">${k}</a></li>
%endfor
</ul>
</div>
<div class="similar">
<h1>Similar Artists</h1>
<table id="similar">
%for i, entry in enumerate(similar) :
<tr><td><img width="64" src="${entry['image_url'] | h}" alt="${entry['image_url'] | h}"/></td><td>${entry['name']} ${entry['similarity']}% similar</td></tr>
%endfor
</table>
</div>
<p>${datasource}</p>
%else:
<h1>${ _("Unable to retrieve artist information:") }</h1>
<p class="error">${error | h}</p>
%endif
</body>
</html>
+9
Ver Arquivo
@@ -0,0 +1,9 @@
<%page args="stylesheet"/>
<html>
<head>
<meta http-equiv="content-type" content="text-html; charset=utf-8">
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
</head>
<body>
</body>
</html>
+13
Ver Arquivo
@@ -0,0 +1,13 @@
body { padding: 6px; line-height: 1.4em }
h1 { font-size: 100% }
h2 { font-size: 90%; color: #555; line-height: 1em }
h3 {padding: 0}
ol { padding: 5px 0 0 15px }
.hidden { display: none }
.shown { display: block }
.album img { float: left; margin: 0 5px 5px 0; padding: 1px; border: 1px solid #AAA }
.artist img { float: left; margin: 0 7px 4px 0; padding: 1px; border: 1px solid #AAA }
.album {clear: both; }
.duration {font-size: 80%; font-style: italic}
button {align: right}
table {font-size: 100%; width: 100%; display:block; clear: both; }
+30
Ver Arquivo
@@ -0,0 +1,30 @@
<%page args="error, artist, album, art_links, alb_links, images, stylesheet" />
<html>
<head>
<meta http-equiv="content-type" content="text-html; charset=utf-8">
<link rel="stylesheet" href="${stylesheet}" type="text/css" />
<style type="text/css">
img { padding: 0 5px 0 5px }
ul { list-style-type: none }
</style>
</head>
<body>
%if error is None:
<h1>${ _("Links for %s:") % ("<em>" + artist + "</em>")}</h1>
<ul>
%for k, v in art_links.items() :
<li><img src="${images}${k}16x16.png" /><a href="${v}">${k}</a></li>
%endfor
</ul>
<h1>${ _("Links for %s:") % ("<em>" + album + "</em>")}</h1>
<ul>
%for k, v in alb_links.items() :
<li><img src="${images}${k}16x16.png" /><a href="${v}">${k}</a></li>
%endfor
</ul>
%else:
<h1>${ _("Unable to get links") }</h1>
<p class="error">${error | h}</p>
%endif
</body>
</html>
+14
Ver Arquivo
@@ -0,0 +1,14 @@
<html>
<head>
<meta http-equiv="content-type" content="text-html; charset=utf-8">
<style type="text/css">
body { padding: 6px; line-height: 1.4em }
h1 { font-size: 130% }
img { display: block; margin-left: auto; margin-right: auto }
</style>
</head>
<body>
<h1>${info | h}</h1>
<img src="${basepath}/img/spinner.gif" />
</body>
</html>
+57 -15
Ver Arquivo
@@ -10,7 +10,7 @@
<object class="GtkListStore" id="liststore1">
<columns>
<!-- column-name artist name -->
<column type="gchararray"/>
<column type="gchararray" />
</columns>
</object>
<object class="GtkBox" id="main_box">
@@ -45,14 +45,14 @@
<property name="model">liststore1</property>
<child internal-child="selection">
<object class="GtkTreeSelection" id="artist_treeview_selection">
<signal name="changed" handler="on_artist_treeview_selection_changed" swapped="no"/>
<signal name="changed" handler="on_artist_treeview_selection_changed" swapped="no" />
</object>
</child>
<child>
<object class="GtkTreeViewColumn" id="treeviewcolumn1">
<property name="title" translatable="yes">Track Artist</property>
<child>
<object class="GtkCellRendererText" id="cellrenderertext1"/>
<object class="GtkCellRendererText" id="cellrenderertext1" />
<attributes>
<attribute name="text">0</attribute>
</attributes>
@@ -61,13 +61,19 @@
</child>
</object>
</child>
</object>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<child>
<object class="GtkPaned" id="vertical_info_paned">
<property name="visible">False</property>
<property name="can_focus">True</property>
<property name="orientation">horizontal</property>
<property name="position">0</property>
<child>
<object class="GtkOverlay" id="iconview_overlay">
<property name="visible">True</property>
<child>
@@ -77,15 +83,14 @@
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child>
</child>
<child></child>
</object>
</child>
<child type="overlay">
<object class="QuickSearchEntry" id="quick_search_entry">
<property name="shadow_type">out</property>
<property name="halign">end</property>
<property name="valign">end</property>
<property name="valign">end</property>
</object>
</child>
</object>
@@ -94,26 +99,63 @@
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkBox" id="box1">
<property name="visible">True</property>
<property name="can_focus">False</property>
<property name="orientation">vertical</property>
<child>
<object class="GtkBox" id="info_button_box">
<property name="visible">True</property>
<property name="can_focus">False</property>
<child>
<placeholder/>
</child>
</object>
<packing>
<property name="expand">False</property>
<property name="fill">False</property>
<property name="position">0</property>
</packing>
</child>
<child>
<object class="GtkScrolledWindow" id="info_scrolled_window">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="hexpand">True</property>
<property name="vexpand">True</property>
<property name="shadow_type">in</property>
<child></child>
</object>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
</object>
<packing>
<property name="resize">True</property>
<property name="shrink">True</property>
</packing>
</child>
<child>
<object class="GtkNotebook" id="bottom_notebook">
<property name="visible">True</property>
<property name="can_focus">True</property>
<property name="vexpand">True</property>
<property name="tab_pos">bottom</property>
<signal name="switch-page" handler="notebook_switch_page_callback" swapped="no"/>
<signal name="switch-page" handler="notebook_switch_page_callback" swapped="no" />
</object>
</child>
</object>
</object>
</child>
<child>
<object class="GtkLabel" id="status_label">
@@ -173,7 +215,7 @@
<property name="tooltip_text" translatable="yes">Stop coverart fetch.</property>
<property name="use_action_appearance">False</property>
<property name="image">cancel-icon</property>
<signal name="clicked" handler="cancel_request_callback" swapped="no"/>
<signal name="clicked" handler="cancel_request_callback" swapped="no" />
</object>
<packing>
<property name="expand">False</property>