[rhythmbox] Context pane plugin



commit 52d375772eb7090f7e0cec5ca0b4e3192d90f906
Author: John Iacona <plate0salad gmail com>
Date:   Sat Oct 31 19:25:06 2009 +1000

    Context pane plugin
    
    Imported from git://gitorious.org/rhythmbox-context-pane/rhythmbox-context-pane.git

 plugins/context/AlbumTab.py           |  262 +++++++++++++++++++++++++++
 plugins/context/ArtistTab.py          |  317 +++++++++++++++++++++++++++++++++
 plugins/context/ContextView.py        |  223 +++++++++++++++++++++++
 plugins/context/LeoslyricsParser.py   |  115 ++++++++++++
 plugins/context/LyrcParser.py         |   86 +++++++++
 plugins/context/LyricsParse.py        |   67 +++++++
 plugins/context/LyricsTab.py          |  174 ++++++++++++++++++
 plugins/context/Makefile.am           |   27 +++
 plugins/context/__init__.py           |   41 +++++
 plugins/context/context.rb-plugin     |    9 +
 plugins/context/context/LyricsTab.py  |  157 ++++++++++++++++
 plugins/context/context/Makefile.am   |   11 ++
 plugins/context/img/spinner.gif       |  Bin 0 -> 5011 bytes
 plugins/context/tmpl/album-tmpl.html  |   75 ++++++++
 plugins/context/tmpl/artist-tmpl.html |   45 +++++
 plugins/context/tmpl/loading.html     |   14 ++
 plugins/context/tmpl/lyrics-tmpl.html |   10 +
 plugins/context/tmpl/main.css         |   13 ++
 18 files changed, 1646 insertions(+), 0 deletions(-)
---
diff --git a/plugins/context/AlbumTab.py b/plugins/context/AlbumTab.py
new file mode 100644
index 0000000..2a17bdc
--- /dev/null
+++ b/plugins/context/AlbumTab.py
@@ -0,0 +1,262 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 rb, rhythmdb
+import gtk, gobject
+import webkit
+import os
+from mako.template import Template
+import xml.dom.minidom as dom
+
+class AlbumTab (gobject.GObject) : 
+
+    __gsignals__ = {
+        'switch-tab' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                                (gobject.TYPE_STRING,))
+    }
+    
+    def __init__ (self, shell, buttons, ds, view) :
+        gobject.GObject.__init__ (self)
+        self.shell      = shell
+        self.sp         = shell.get_player ()
+        self.db         = shell.get_property ('db') 
+        self.buttons    = buttons
+
+        self.button     = gtk.ToggleButton (_("Albums"))
+        self.ds         = ds
+        self.view       = view
+        self.artist     = None
+        self.active     = False
+
+        self.button.show()
+        self.button.set_relief( gtk.RELIEF_NONE ) 
+        self.button.set_focus_on_click(False)
+        self.button.connect ('clicked', 
+            lambda button : self.emit ('switch-tab', 'album'))
+        buttons.pack_start (self.button, True, True)
+
+    def activate (self) :
+        self.button.set_active(True)
+        self.active = True
+        self.reload ()
+
+    def deactivate (self) :
+        self.button.set_active(False)
+        self.active = False
+
+    def reload (self) :
+        entry = self.sp.get_playing_entry ()
+        if entry is None : return None
+
+        artist = self.db.entry_get (entry, rhythmdb.PROP_ARTIST)
+        album  = self.db.entry_get (entry, rhythmdb.PROP_ALBUM)
+        if self.active and artist != self.artist :
+            self.view.loading(artist)
+            self.ds.fetch_album_list (artist)
+        else :
+            self.view.load_view()
+
+        self.artist = artist
+
+class AlbumView (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    = ""
+
+        self.basepath = "file://" + os.path.split(plugin.find_file('AlbumTab.py'))[0]
+
+        self.load_tmpl ()
+        self.connect_signals ()
+
+    def load_view (self) :
+        self.webview.load_string(self.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) :
+        self.loading_file = self.loading_template.render (
+            artist   = current_artist,
+            info     = "Top Albums",
+            song     = "",
+            basepath = self.basepath)
+        self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
+
+    def load_tmpl (self) :
+        self.path = self.plugin.find_file ('tmpl/album-tmpl.html')
+        self.loading_path = self.plugin.find_file ('tmpl/loading.html')
+        self.album_template = Template (filename = self.path,
+                                        module_directory = '/tmp/context')
+        self.loading_template = Template (filename = self.loading_path, 
+                                          module_directory = '/tmp/context')
+        self.styles = self.basepath + '/tmpl/main.css'
+
+    def album_list_ready (self, ds) :
+        list = ds.get_top_albums ()
+        self.file = self.album_template.render (error = ds.get_error(), 
+                                                list = list, 
+                                                artist = ds.get_artist(),
+                                                stylesheet = self.styles)
+        self.load_view ()
+
+
+class AlbumDataSource (gobject.GObject) :
+    
+    __gsignals__ = {
+        'albums-ready' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ())
+    }
+
+    def __init__ (self) :
+        gobject.GObject.__init__ (self)
+        self.api_key = '27151108bfce62e12c1f6341437e0e83'
+        self.url_prefix = 'http://ws.audioscrobbler.com/2.0/?method='
+        self.albums = None
+        self.error = None
+        self.max_albums_fetched = 8
+
+    def extract (self, data, position) :
+        """
+        Safely extract the data from an xml node. Returns data
+        at position or None if position does not exist
+        """
+        try :
+            return data[position].firstChild.data
+        except Exception, e :
+            return None
+
+    def get_artist (self) :	
+	return self.artist
+
+    def get_error (self) :
+        return self.error
+
+    def fetch_album_list (self, artist) :
+    	self.artist = artist
+        self.error  = None
+        url = "%sartist.gettopalbums&artist=%s&api_key=%s" % (self.url_prefix,
+                                                              artist.replace(" ", "+"),
+                                                              self.api_key)
+        try :
+            ld = rb.Loader ()
+            ld.get_url (url, self.fetch_album_list_cb, artist) 
+        except Exception, e :
+            print "problem fetching %s: %s" % (artist, e)
+            return
+
+    def fetch_album_list_cb (self, data, artist) :
+        if data is None : 
+            print "Nothing fetched for %s top albums" % artist
+            return
+
+        parsed = dom.parseString (data)
+        lfm = parsed.getElementsByTagName ('lfm')[0]
+        if lfm.attributes['status'].value == 'failed' :
+            self.error = lfm.childNodes[1].firstChild.data
+            self.emit ('albums-ready')
+            return
+
+        self.albums = []
+        album_nodes = parsed.getElementsByTagName ('album') 
+        print "num albums: %d" % len(album_nodes)
+        if len(album_nodes) == 0 :
+            self.error = "No albums found for %s" % artist
+            self.emit('albums-ready')
+            return
+            
+        self.album_info_fetched = min (len (album_nodes) - 1, self.max_albums_fetched)
+
+        for i, album in enumerate (album_nodes) : 
+            if i >= self.album_info_fetched : break
+
+            album_name = self.extract(album.getElementsByTagName ('name'), 0)
+            imgs = album.getElementsByTagName ('image')
+            images = (self.extract(imgs, 0), self.extract(imgs, 1), self.extract(imgs, 2))
+            self.albums.append ({'title' : album_name, 'images' : images })
+            self.fetch_album_info (artist, album_name, i)
+
+    def get_top_albums (self) :
+        return self.albums
+
+    def fetch_album_info (self, artist, album, index) :
+        url = "%salbum.getinfo&artist=%s&album=%s&api_key=%s" % (self.url_prefix,
+                                                                 artist.replace(" ", "+"),
+                                                                 album.replace(" ", "+"),
+                                                                 self.api_key)
+
+        ld = rb.Loader()
+        ld.get_url (url, self.fetch_album_tracklist, album, index)
+            
+    def fetch_album_tracklist (self, data, album, index) :
+        if data is None :
+            self.assemble_info(None, None, None)
+
+        parsed = dom.parseString (data)
+        
+        try :
+            self.albums[index]['id'] = parsed.getElementsByTagName ('id')[0].firstChild.data
+        except Exception, e :
+            print "Problem parsing id, exiting: %s" % e
+            return None
+
+        self.albums[index]['releasedate'] = self.extract(parsed.getElementsByTagName ('releasedate'),0)
+        self.albums[index]['summary'] = self.extract(parsed.getElementsByTagName ('summary'), 0)
+
+        url = "%splaylist.fetch&playlistURL=lastfm://playlist/album/%s&api_key=%s" % (
+                     self.url_prefix, self.albums[index]['id'], self.api_key)
+
+        ld = rb.Loader()
+        ld.get_url (url, self.assemble_info, album, index)
+
+    def assemble_info (self, data, album, index) :
+        if data is None :
+            print "nothing fetched for %s tracklist" % album
+        else :
+            parsed = dom.parseString (data)
+            try :
+                list = parsed.getElementsByTagName ('track')
+                tracklist = []
+                album_length = 0
+                for i, track in enumerate(list) :
+                    title = track.getElementsByTagName ('title')[0].firstChild.data
+                    duration = int(track.getElementsByTagName ('duration')[0].firstChild.data) / 1000
+                    album_length += duration
+                    tracklist.append ((i, title, duration))
+                self.albums[index]['tracklist'] = tracklist
+                self.albums[index]['duration']  = album_length
+            except Exception, e :
+                print "Problem : %s" % e
+
+        gtk.gdk.threads_enter ()
+        self.album_info_fetched -= 1
+        print "%s albums left to process" % self.album_info_fetched
+        gtk.gdk.threads_leave ()
+        if self.album_info_fetched == 0 :
+            self.emit('albums-ready')
diff --git a/plugins/context/ArtistTab.py b/plugins/context/ArtistTab.py
new file mode 100644
index 0000000..301dc41
--- /dev/null
+++ b/plugins/context/ArtistTab.py
@@ -0,0 +1,317 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 rb, rhythmdb
+import gtk, gobject
+import re, os
+import xml.dom.minidom as dom
+
+import webkit
+from mako.template import Template
+    
+class ArtistTab (gobject.GObject) : 
+    
+    __gsignals__ = {
+        'switch-tab' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                                (gobject.TYPE_STRING,))
+    }
+
+    def __init__ (self, shell, buttons, ds, view) :
+        gobject.GObject.__init__ (self)
+        self.shell      = shell
+        self.sp         = shell.get_player ()
+        self.db         = shell.get_property ('db') 
+        self.buttons    = buttons
+
+        self.button     = gtk.ToggleButton (_("Artist"))
+        self.datasource = ds
+        self.view       = view
+        self.artist     = None
+        self.active     = False
+
+        self.button.show()
+        self.button.set_relief( gtk.RELIEF_NONE ) 
+        self.button.set_focus_on_click(False)
+        self.button.connect ('clicked', 
+            lambda button : self.emit('switch-tab', 'artist'))
+        buttons.pack_start (self.button, True, True)
+
+    def activate (self) :
+        print "activating Artist Tab"
+        self.button.set_active(True)
+        self.active = True
+        self.reload ()
+
+    def deactivate (self) :
+        print "deactivating Artist Tab"
+        self.button.set_active(False)
+        self.active = False
+
+    def reload (self) :
+        entry = self.sp.get_playing_entry ()
+        if entry is None : 
+            print "Nothing playing"
+            return None
+        artist = self.db.entry_get (entry, rhythmdb.PROP_ARTIST)
+
+        if self.active and self.artist != artist  :
+            self.datasource.fetch_artist_data (artist)
+            self.view.loading (artist)
+        else :
+            self.view.load_view()
+        self.artist = artist
+
+class ArtistView (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     = ""
+        self.basepath = 'file://' + os.path.split(plugin.find_file('AlbumTab.py'))[0]
+
+        self.load_tmpl ()
+        self.connect_signals ()
+
+    def load_view (self) :
+        self.webview.load_string (self.file, 'text/html', 'utf-8', self.basepath)
+
+    def loading (self, current_artist) :
+        self.loading_file = self.loading_template.render (
+            artist   = current_artist,
+            info     = "Bio",
+            song     = "",
+            basepath = self.basepath)
+        self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
+
+    def load_tmpl (self) :
+        self.path = self.plugin.find_file('tmpl/artist-tmpl.html')
+        self.loading_path = self.plugin.find_file ('tmpl/loading.html')
+        self.template = Template (filename = self.path, module_directory = '/tmp/context/')
+        self.loading_template = Template (filename = self.loading_path, module_directory = '/tmp/context')
+        self.styles = self.basepath + '/tmpl/main.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']
+            summary, full_bio = info['bio'] 
+            self.file = self.template.render (artist     = ds.get_current_artist (),
+                                              image      = med,
+                                              fullbio    = full_bio,
+                                              shortbio   = summary,
+                                              stylesheet = self.styles )
+            self.load_view ()
+        except Exception, e :
+            print "Problem in info ready: %s" % e
+    
+
+class ArtistDataSource (gobject.GObject) :
+    
+    __gsignals__ = {
+        'artist-info-ready'       : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        'artist-similar-ready'    : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        'artist-top-tracks-ready' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+        'artist-top-albums-ready' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+    def __init__ (self) :
+        gobject.GObject.__init__ (self)
+        self.api_key = '27151108bfce62e12c1f6341437e0e83'
+        self.url_prefix = 'http://ws.audioscrobbler.com/2.0/?method='
+
+        self.current_artist = None
+        self.artist = {
+            'info' : {
+                'data'      : None, 
+                'signal'    : 'artist-info-ready', 
+                'function'  : 'getinfo',
+                'parsed'    : False,
+            },
+            
+            'similar' : {
+                'data'      : None, 
+                'signal'    : 'artist-similar-ready', 
+                'function'  : 'getsimilar',
+                'parsed'    : False,
+            },
+
+            'top_albums' : {
+                'data'      : None, 
+                'signal'    : 'artist-top-albums-ready',
+                'function'  : 'gettopalbums',
+                'parsed'    : False,
+            },
+
+            'top_tracks' : {
+                'data'      : None, 
+                'signal'    : 'artist-top-tracks-ready',
+                'function'  : 'gettoptracks',
+                'parsed'    : False,
+            },
+        }
+       
+    def extract (self, data, position) :
+        """
+        Safely extract the data from an xml node. Returns data
+        at position or None if position does not exist
+        """
+        
+        try :
+            return data[position].firstChild.data
+        except Exception, e :
+            return None
+
+    def fetch_top_tracks (self, artist) :
+        artist = artist.replace (" ", "+")
+        url = '%sartist.%s&artist=%s&api_key=%s' % (self.url_prefix,
+            self.artist['top_tracks']['function'], artist, self.api_key)
+        ld = rb.Loader()
+        ld.get_url (url, self.fetch_artist_data_cb, self.artist['top_tracks'])
+
+    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
+        artist = artist.replace(" ", "+")
+        for key, value in self.artist.items() :
+            url = '%sartist.%s&artist=%s&api_key=%s' % (self.url_prefix,
+                value['function'], artist, self.api_key)
+            ld = rb.Loader()
+            ld.get_url (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
+        category['data'] = dom.parseString (data)
+        category['parsed'] = False
+        self.emit (category['signal'])
+
+    def get_current_artist (self) :
+        return self.current_artist
+
+    def get_top_albums (self) :
+        if not self.artist['top_albums']['parsed'] :
+            albums = []
+            for album in self.artist['top_albums']['data'].getElementsByTagName ('album') :
+                album_name = self.extract(album.getElementsByTagName ('name'), 0)
+                imgs = album.getElementsByTagName ('image') 
+                images = self.extract(imgs, 0), self.extract(imgs, 1), self.extract(imgs,2)
+                albums.append ((album_name, images))
+            self.artist['top_albums']['data'] = albums
+            self.artist['top_albums']['parsed'] = True
+
+        return self.artist['top_albums']['data']
+
+    def get_similar_artists (self) :
+        """
+        Returns a list of similar artists
+        """
+        data = self.artist['similar']['data']
+        if data is None :
+            return None
+
+        if not self.artist['similar']['parsed'] :
+            lst = []
+            for node in data.getElementsByTagName ('artist') :
+                artist = self.extract(node.getElementsByTagName('name'), 0)
+                similar = self.extract(node.getElementsByTagName('match') ,0)
+                image = self.extract(node.getElementsByTagName('image'), 0)
+                lst.append ((artist, similar, image))
+            data = lst
+            self.artist['similar']['parsed'] = True
+            self.artist['similar']['data'] = data
+
+        return data
+
+    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 = data.getElementsByTagName ('image')
+        return self.extract(images,0), self.extract(images,1), self.extract(images,2)
+        
+    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 = self.extract(data.getElementsByTagName ('content'), 0)
+            summary = self.extract(data.getElementsByTagName ('summary'), 0)
+            return summary, content
+
+        return self.artist['info']['data']['bio']
+
+    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']
+
+    def get_top_tracks (self) :
+        """
+        Returns a list of the top track titles
+        """
+        data = self.artist['top_tracks']['data']
+        if data is None :
+            return None
+
+        if not self.artist['top_tracks']['parsed'] :
+            tracks = []
+            for track in data.getElementsByTagName ('track') :
+                name = self.extract(track.getElementsByTagName('name'), 0)
+                tracks.append (name)
+            self.artist['top_tracks']['data'] = tracks
+            self.artist['top_tracks']['parsed'] = True
+
+        return self.artist['top_tracks']['data']
diff --git a/plugins/context/ContextView.py b/plugins/context/ContextView.py
new file mode 100644
index 0000000..31aa688
--- /dev/null
+++ b/plugins/context/ContextView.py
@@ -0,0 +1,223 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 rb, rhythmdb
+import gtk, gobject
+import pango
+import webkit
+
+import ArtistTab as at
+import AlbumTab as abt
+import LyricsTab as lt
+
+context_ui = """
+<ui>
+    <toolbar name="ToolBar">
+        <toolitem name="Context" action="ToggleContextView" />
+    </toolbar>
+</ui>
+"""
+
+class ContextView (gobject.GObject) :
+
+    def __init__ (self, shell, plugin) :
+        gobject.GObject.__init__ (self)
+	self.shell = shell
+        self.sp = shell.get_player ()
+        self.db = shell.get_property ('db')
+        self.plugin = plugin
+        
+        self.top_five = None
+        self.current_artist = None
+        self.current_album = None
+        self.current_song = None
+        self.visible = True
+        
+	self.init_gui ()
+        self.init_tabs()
+
+        self.connect_signals ()
+        self.load_top_five (self.ds['artist'])
+
+        # Set currently displayed tab
+        # TODO: make this persistent via gconf key
+        self.current = 'artist'
+        self.tab[self.current].activate ()
+
+        # Add button to toggle visibility of panel
+        self.action = ('ToggleContextView','gtk-info', _('Toggle Conte_xt Pane'),
+                        None, _('Change the visibility of the context pane'),
+                        self.toggle_visibility, True)
+        self.action_group = gtk.ActionGroup('ContextPluginActions')
+        self.action_group.add_toggle_actions([self.action])
+        uim = self.shell.get_ui_manager()
+        uim.insert_action_group (self.action_group, 0)
+        self.ui_id = uim.add_ui_from_string(context_ui)
+        uim.ensure_update()
+
+    def deactivate (self, shell) :
+        self.shell = None
+        self.disconnect_signals ()
+        self.player_cb_ids = None
+        self.tab_cb_ids = None
+        self.sp = None
+        self.db = None
+        self.plugin = None
+        self.top_five = None
+        self.tab = None
+        shell.remove_widget (self.vbox, rb.SHELL_UI_LOCATION_RIGHT_SIDEBAR)
+        uim = shell.get_ui_manager ()
+        uim.remove_ui (self.ui_id)
+        uim.remove_action_group (self.action_group)
+
+    def connect_signals(self) :
+        self.player_cb_ids = ( self.sp.connect ('playing-changed', self.playing_changed_cb),
+            self.sp.connect ('playing-song-changed', self.playing_changed_cb))
+        self.ds_cb_id = self.ds['artist'].connect ('artist-top-tracks-ready', self.load_top_five)
+        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)))
+
+    def disconnect_signals (self) :
+        for id in self.player_cb_ids :
+            self.sp.disconnect (id)
+
+        self.ds['artist'].disconnect (self.ds_cb_id)
+
+        for key, id in self.tab_cb_ids :
+            self.tab[key].disconnect (id)
+
+    def toggle_visibility (self, action) :
+        if not self.visible :
+            self.shell.add_widget (self.vbox, rb.SHELL_UI_LOCATION_RIGHT_SIDEBAR, expand=True)
+            self.visible = True
+        else :
+            self.shell.remove_widget (self.vbox, rb.SHELL_UI_LOCATION_RIGHT_SIDEBAR)
+            self.visible = False
+
+    def change_tab (self, tab, newtab) :
+        print "swaping tab from %s to %s" % (self.current, newtab)
+        if (self.current != newtab) :
+            self.tab[self.current].deactivate()
+            self.tab[newtab].activate()
+            self.current = newtab
+
+    def init_tabs (self) :
+        self.tab = {}
+        self.ds = {}
+        self.view = {}
+
+        self.ds['artist'] = at.ArtistDataSource ()
+        self.view['artist'] = at.ArtistView (self.shell, self.plugin, self.webview, self.ds['artist'])
+        self.tab['artist']  = at.ArtistTab (self.shell, self.buttons, self.ds['artist'], self.view['artist'])
+        self.ds['album']    = abt.AlbumDataSource()
+        self.view['album']  = abt.AlbumView(self.shell, self.plugin, self.webview, self.ds['album'])
+        self.tab['album']   = abt.AlbumTab(self.shell, self.buttons, self.ds['album'], self.view['album'])
+        self.ds['lyrics']   = lt.LyricsDataSource ()
+        self.view['lyrics'] = lt.LyricsView (self.shell, self.plugin, self.webview, self.ds['lyrics'])
+        self.tab['lyrics']  = lt.LyricsTab (self.shell, self.buttons, self.ds['lyrics'], self.view['lyrics'])
+
+    def load_top_five (self, ds) :
+        top_tracks = ds.get_top_tracks ()
+        ## populate liststore
+        if top_tracks is None :
+            self.top_five = ['','','','','']
+            for i in range (0, 5) :
+                self.top_five_list.append(["%d. " % (i+1), ""])
+        else :
+            num_tracks = len(top_tracks)
+            for i in range (0, 5) :
+                if i >= num_tracks : track = ""
+                else : track = top_tracks[i]
+                self.top_five_list[(i,)] = ("%d. " % (i+1), track)
+
+    def playing_changed_cb (self, playing, user_data) :
+        playing_entry = self.sp.get_playing_entry ()
+        if playing_entry is None : return
+
+        playing_artist = self.db.entry_get (playing_entry, rhythmdb.PROP_ARTIST)
+
+        if self.current_artist != playing_artist :
+            self.current_artist = playing_artist.replace ('&', '&amp;')
+            self.label.set_markup(_('Top songs by <i>%s</i>' % self.current_artist))
+            self.ds['artist'].fetch_top_tracks (self.current_artist)
+
+        self.tab[self.current].reload()
+
+    def init_gui(self) :
+        self.vbox = gtk.VBox()
+        self.frame = gtk.Frame()
+        self.label = gtk.Label(_('Nothing Playing'))
+        self.frame.set_shadow_type(gtk.SHADOW_IN)
+        self.frame.set_label_align(0.0,0.0)
+        self.frame.set_label_widget(self.label)
+        self.label.set_use_markup(True)
+        self.label.set_padding(0,4)
+
+        #----- set up top 5 tree view -----#
+        self.top_five_list = gtk.ListStore (gobject.TYPE_STRING, gobject.TYPE_STRING)
+        self.top_five_view = gtk.TreeView(self.top_five_list)
+
+        self.top_five_tvc1 = gtk.TreeViewColumn()
+        self.top_five_tvc2 = gtk.TreeViewColumn()
+
+        self.top_five_view.append_column(self.top_five_tvc1)
+        self.top_five_view.append_column(self.top_five_tvc2)
+
+        self.crt = gtk.CellRendererText()
+
+        self.top_five_tvc1.pack_start(self.crt, True)
+        self.top_five_tvc2.pack_start(self.crt, True)
+
+        self.top_five_tvc1.add_attribute(self.crt, 'text', 0)
+        self.top_five_tvc2.add_attribute(self.crt, 'text', 1)
+        
+        self.top_five_view.set_headers_visible( False )
+        self.frame.add (self.top_five_view)
+
+        #---- set up webkit pane -----#
+        self.webview = webkit.WebView()
+        self.scroll = gtk.ScrolledWindow()
+        self.scroll.set_policy(gtk.POLICY_NEVER, gtk.POLICY_AUTOMATIC)
+        self.scroll.set_shadow_type(gtk.SHADOW_IN)
+        self.scroll.add( self.webview )
+
+        #----- set up button group -----#
+        self.vbox2 = gtk.VBox()
+        self.buttons = gtk.HBox()
+
+        #---- pack everything into side panel ----#
+        self.vbox.pack_start  (self.frame, expand = False)
+        self.vbox2.pack_start (self.buttons, expand = False)
+        self.vbox2.pack_start (self.scroll, expand = True)
+        self.vbox.pack_start  (self.vbox2, expand = True)
+
+        self.vbox.show_all()
+        self.vbox.set_size_request(200, -1)
+        self.shell.add_widget (self.vbox, rb.SHELL_UI_LOCATION_RIGHT_SIDEBAR, expand=True)
+
diff --git a/plugins/context/LeoslyricsParser.py b/plugins/context/LeoslyricsParser.py
new file mode 100644
index 0000000..8a9d74e
--- /dev/null
+++ b/plugins/context/LeoslyricsParser.py
@@ -0,0 +1,115 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# Copyright (C) 2006 Jonathan Matthew
+# Copyright (C) 2007 James Livingston
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 urllib
+import re
+import rb
+
+from rb.stringmatch import string_match
+
+# these numbers pulled directly from the air
+artist_match = 0.8
+title_match = 0.5
+
+# Python 2.4 compatibility
+try:
+	from xml.etree import cElementTree
+except:
+	import cElementTree
+
+
+
+class LeoslyricsParser(object):
+	def __init__(self, artist, title):
+		self.artist = artist
+		self.title = title
+	
+	def search(self, callback, *data):
+		artist = urllib.quote(self.artist)
+		title = urllib.quote(self.title)
+
+		htstring = 'http://api.leoslyrics.com/api_search.php?auth=Rhythmbox&artist=%s&songtitle=%s' % (artist, title)
+			
+		loader = rb.Loader()
+		loader.get_url (htstring, self.got_lyrics, callback, *data)
+
+	def got_lyrics (self, lyrics, callback, *data):
+		if lyrics is None:
+			callback (None, *data)
+			return
+
+		element = cElementTree.fromstring(lyrics)
+		if element.find("response").attrib['code'] is not '0':
+			print "got failed response:" + lyrics
+			callback (None, *data)
+			return
+
+		match = None
+		matches = element.find("searchResults").findall("result")
+		print "got %d result(s)" % (len(matches))
+		for m in matches:
+			matchtitle = m.findtext("title")
+			matchartist = m.findtext("artist/name")
+
+			# if we don't know the artist, then anyone will do
+			if self.artist != "":
+				artist_str = string_match(self.artist, matchartist)
+			else:
+				artist_str = artist_match + 0.1
+
+			title_str = string_match(self.title, matchtitle)
+			if artist_str > artist_match and title_str > title_match:
+				print "found acceptable match, artist: %s (%f), title: %s (%f)" % (matchartist, artist_str, matchtitle, title_str)
+				match = m
+				break
+			else:
+				print "skipping match, artist: %s (%f), title: %s (%f)" % (matchartist, artist_str, matchtitle, title_str)
+
+		if match is not None:
+			hid = m.attrib['hid'].encode('utf-8')
+			lurl = "http://api.leoslyrics.com/api_lyrics.php?auth=Rhythmbox&hid=%s"; % (urllib.quote(hid))
+			loader = rb.Loader()
+			loader.get_url (lurl, self.parse_lyrics, callback, *data)
+		else:
+			print "no acceptable match found"
+			callback (None, *data)
+
+
+	def parse_lyrics(self, result, callback, *data):
+		if result is None:
+			callback (None, *data)
+			return
+
+		element = cElementTree.fromstring(result)
+
+		lyrics = element.find('lyric').find('text').text
+		lyrics += "\n\nLyrics provided by leoslyrics.com"
+
+		callback (lyrics, *data)
+
diff --git a/plugins/context/LyrcParser.py b/plugins/context/LyrcParser.py
new file mode 100644
index 0000000..0881e22
--- /dev/null
+++ b/plugins/context/LyrcParser.py
@@ -0,0 +1,86 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# Copyright (C) 2007 James Livingston
+# Copyright (C) 2007 Sirio Bolaños Puchet
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 urllib
+import re
+import rb
+
+EXPS = ['\n', '\r', '<[iI][mM][gG][^>]*>', '<[aA][^>]*>[^<]*<\/[aA]>',
+	'<[sS][cC][rR][iI][pP][tT][^>]*>[^<]*(<!--[^>]*>)*[^<]*<\/[sS][cC][rR][iI][pP][tT]>',
+	'<[sS][tT][yY][lL][eE][^>]*>[^<]*(<!--[^>]*>)*[^<]*<\/[sS][tT][yY][lL][eE]>']
+CEXPS = [re.compile (exp) for exp in EXPS]
+
+SEPARATOR_RE = re.compile("<[fF][oO][nN][tT][ ]*[sS][iI][zZ][eE][ ]*='2'[ ]*>")
+
+
+class LyrcParser (object):
+	def __init__(self, artist, title):
+		self.artist = artist
+		self.title = title
+	
+	def search(self, callback, *data):
+		path = 'http://www.lyrc.com.ar/en/'
+
+		wartist = urllib.quote(self.artist)
+		wtitle = urllib.quote(self.title)
+		wurl = 'tema1en.php?artist=%s&songname=%s' % (wartist, wtitle)
+		
+		loader = rb.Loader()
+		loader.get_url (path + wurl, self.got_lyrics, callback, *data)
+
+	def got_lyrics(self, lyrics, callback, *data):
+		if lyrics is None:
+			callback (None, *data)
+			return
+
+		for exp in CEXPS:
+			lyrics = exp.sub('', lyrics)
+
+		lyricIndex = SEPARATOR_RE.search(lyrics)
+
+		if lyricIndex is not None:
+			callback(self.parse_lyrics(SEPARATOR_RE.split(lyrics, 1)[1]), *data)
+		else:
+			callback (None, *data)
+
+	def parse_lyrics(self, lyrics):
+		if re.search('<p><hr', lyrics):
+			lyrics = re.split('<p><hr', lyrics, 1)[0]
+		else:
+			lyrics = re.split('<br><br>', lyrics, 1)[0]
+		
+		lyrics = re.sub('<[fF][oO][nN][tT][^>]*>', '', lyrics)
+		title = re.split('(<[bB]>)([^<]*)', lyrics)[2]
+		artist = re.split('(<[uU]>)([^<]*)', lyrics)[2]
+		lyrics = re.sub('<[bB]>[^<].*<\/[tT][aA][bB][lL][eE]>', '', lyrics)
+		lyrics = re.sub('<[Bb][Rr][^>]*>', '\n', lyrics)
+		titl = "%s - %s\n\n" % (artist, title)
+		lyrics = titl + lyrics
+		lyrics += "\n\nLyrics provided by lyrc.com.ar"
+
+		return lyrics
+
diff --git a/plugins/context/LyricsParse.py b/plugins/context/LyricsParse.py
new file mode 100644
index 0000000..3994c27
--- /dev/null
+++ b/plugins/context/LyricsParse.py
@@ -0,0 +1,67 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# Copyright (C) 2007 James Livingston
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 urllib
+import re
+import gobject
+import gconf
+import rb
+
+from LyrcParser import LyrcParser
+from LeoslyricsParser import LeoslyricsParser
+
+engines = {
+	'lyrc.com.ar': LyrcParser,
+	'leoslyrics.com': LeoslyricsParser
+}
+
+
+class Parser (object):
+	def __init__(self, artist, title):
+		self.title = title
+		self.artist = artist
+
+	def searcher(self, plexer, callback, *data):
+		for name, engine in engines.iteritems():
+			plexer.clear()
+			print "searching " + name + " for lyrics"
+                        parser = engine (self.artist, self.title)
+
+			parser.search(plexer.send())
+			yield None
+
+			_, (lyrics,) = plexer.receive()
+			if lyrics is not None:
+				callback (lyrics, *data)
+				return
+
+		callback (None, *data)
+
+	def get_lyrics(self, callback, *data):
+		rb.Coroutine (self.searcher, callback, *data).begin ()
+
+
diff --git a/plugins/context/LyricsTab.py b/plugins/context/LyricsTab.py
new file mode 100644
index 0000000..5b81e49
--- /dev/null
+++ b/plugins/context/LyricsTab.py
@@ -0,0 +1,174 @@
+import rb, rhythmdb
+import gtk, gobject
+import urllib
+import re, os
+from rb.stringmatch import string_match
+from mako.template import Template
+import LyricsParse
+
+LYRIC_TITLE_STRIP=["\(live[^\)]*\)", "\(acoustic[^\)]*\)", "\([^\)]*mix\)", "\([^\)]*version\)", "\([^\)]*edit\)", "\(feat[^\)]*\)"]
+LYRIC_TITLE_REPLACE=[("/", "-"), (" & ", " and ")]
+LYRIC_ARTIST_REPLACE=[("/", "-"), (" & ", " and ")]
+
+class LyricsTab (gobject.GObject) :
+    
+    __gsignals__ = {
+        'switch-tab' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                                (gobject.TYPE_STRING,))
+    }
+
+    def __init__ (self, shell, toolbar, ds, view) :
+        gobject.GObject.__init__ (self)
+        self.shell      = shell
+        self.sp         = shell.get_player ()
+        self.db         = shell.get_property ('db') 
+        self.toolbar    = toolbar
+
+        self.button     = gtk.ToggleButton (_("Lyrics"))
+        self.datasource = ds
+        self.view       = view
+        self.song       = None
+        
+        self.button.show()
+        self.button.set_relief( gtk.RELIEF_NONE ) 
+        self.button.set_focus_on_click(False)
+        self.button.connect ('clicked', 
+            lambda button : self.emit('switch-tab', 'lyrics'))
+        toolbar.pack_start (self.button, True, True)
+
+    def activate (self) :
+        print "activating Lyrics Tab"
+        self.button.set_active(True)
+        self.reload ()
+
+    def deactivate (self) :
+        print "deactivating Lyrics Tab"
+        self.button.set_active(False)
+
+    def reload (self) :
+        entry = self.sp.get_playing_entry ()
+        if entry is None : return
+        song    = self.db.entry_get (entry, rhythmdb.PROP_TITLE)
+        artist  = self.db.entry_get (entry, rhythmdb.PROP_ARTIST)
+        if self.song != song :
+            print "displaying loading screen"
+            self.view.loading(artist, song)
+            print "fetching lyrics"
+            self.datasource.fetch_lyrics (artist, song)
+        else :
+            self.view.load_view()
+        self.song = song
+
+class LyricsView (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    = ""
+        self.basepath = 'file://' + os.path.split(plugin.find_file('AlbumTab.py'))[0]
+
+        self.load_tmpl ()
+        self.connect_signals ()
+
+    def connect_signals (self) :
+        self.ds.connect ('lyrics-ready', self.lyrics_ready)
+
+    def load_view (self) :
+        self.webview.load_string (self.file, 'text/html', 'utf-8', self.basepath)
+
+    def loading (self, current_artist, song) :
+        self.loading_file = self.loading_template.render (
+            artist   = current_artist,
+            info     = "Lyrics",
+            song     = song,
+            basepath = self.basepath)
+        self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
+        print "loading screen loaded"
+
+    def load_tmpl (self) :
+        self.path = self.plugin.find_file('tmpl/lyrics-tmpl.html')
+        self.loading_path = self.plugin.find_file ('tmpl/loading.html')
+        self.template = Template (filename = self.path, 
+                                  module_directory = '/tmp/context/')
+        self.loading_template = Template (filename = self.loading_path, 
+                                          module_directory = '/tmp/context')
+        self.styles = self.basepath + '/tmpl/main.css'
+
+    def lyrics_ready (self, ds) :
+        print "loading lyrics into webview"
+        lyrics = ds.get_lyrics()
+        if lyrics is None : 
+            lyrics = "Lyrics not found"
+        else :
+            lyrics = ds.get_lyrics().strip()
+            lyrics = lyrics.replace ('\n', '<br />')
+
+        self.file = self.template.render (artist     = ds.get_artist (),
+                                          song       = ds.get_title (),
+                                          lyrics     = lyrics,
+                                          stylesheet = self.styles)
+        self.load_view ()
+
+class LyricsDataSource (gobject.GObject) :
+    
+    __gsignals__ = {
+        'lyrics-ready' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, ()),
+    }
+
+    def __init__ (self) :
+        gobject.GObject.__init__ (self)
+        self.artist = "Nothing Playing"
+        self.title = ""
+        self.lyrics = ""
+
+    def fetch_lyrics (self, artist, title) :
+        self.artist = artist
+        self.title = title
+        scrubbed_artist, scrubbed_title = self.parse_song_data (artist, title)
+        parser = LyricsParse.Parser (scrubbed_artist, scrubbed_title)
+        parser.get_lyrics (self.fetch_lyrics_cb)
+
+    def fetch_lyrics_cb (self, lyrics) :
+        self.lyrics = lyrics
+        self.emit ('lyrics-ready')
+
+    def parse_song_data(self, artist, title):
+	# don't search for 'unknown' when we don't have the 
+        # artist or title information
+	if artist == "Unknown" :
+		artist = ""
+	if title == "Unknown" :
+		title = ""
+
+	# convert to lowercase
+	artist = artist.lower()
+	title = title.lower()
+	
+	# replace ampersands and the like
+	for exp in LYRIC_ARTIST_REPLACE:
+		artist = re.sub(exp[0], exp[1], artist)
+	for exp in LYRIC_TITLE_REPLACE:
+		title = re.sub(exp[0], exp[1], title)
+
+	# strip things like "(live at Somewhere)", "(accoustic)", etc
+	for exp in LYRIC_TITLE_STRIP:
+		title = re.sub (exp, '', title)
+
+	# compress spaces
+	title = title.strip()
+	artist = artist.strip()	
+	
+	return (artist, title)
+	
+    def get_lyrics (self) :
+        return self.lyrics
+
+    def get_title (self) :
+        return self.title
+
+    def get_artist (self) :
+        return self.artist 
+
diff --git a/plugins/context/Makefile.am b/plugins/context/Makefile.am
new file mode 100644
index 0000000..5dfa789
--- /dev/null
+++ b/plugins/context/Makefile.am
@@ -0,0 +1,27 @@
+# Context Panel Python Plugin
+
+SUBDIRS = context
+
+plugindir = $(PLUGINDIR)/context
+
+plugin_in_files = context.rb-plugin.in
+%.rb-plugin: %.rb-plugin.in $(INTLTOOL_MERGE) $(wildcard $(top_srcdir)/po/*po) ; $(INTLTOOL_MERGE) $(top_srcdir)/po $< $@ -d -u -c $(top_builddir)/po/.intltool-merge-cache
+
+plugin_DATA = $(plugin_in_files:.rb-plugin.in=.rb-plugin)
+
+tmpldir = $(plugindir)/tmpl
+tmpl_DATA = \
+	tmpl/album-tmpl.html		\
+	tmpl/artist-tmpl.html		\
+	tmpl/loading.html		\
+	tmpl/lyrics-tmpl.html		\
+	tmpl/main.css
+
+imgdir = $(plugindir)/img
+img_DATA = \
+	img/spinner.gif
+
+EXTRA_DIST = $(plugin_in_files) $(tmpl_DATA) $(img_DATA)
+
+CLEANFILES = $(plugin_DATA)
+DISTCLEANFILES = $(plugin_DATA)
diff --git a/plugins/context/__init__.py b/plugins/context/__init__.py
new file mode 100644
index 0000000..0c0d9bc
--- /dev/null
+++ b/plugins/context/__init__.py
@@ -0,0 +1,41 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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.
+
+# vim:shiftwidth=4:softtabstop=4:expandtab
+
+import rb, rhythmdb
+import ContextView as cv
+
+class ContextPlugin(rb.Plugin):
+    def __init__ (self):
+        rb.Plugin.__init__ (self)
+
+    def activate (self, shell):
+        self.context_view = cv.ContextView (shell, self)
+
+    def deactivate(self, shell):
+        self.context_view.deactivate(shell)
+        del self.context_view
diff --git a/plugins/context/context.rb-plugin b/plugins/context/context.rb-plugin
new file mode 100644
index 0000000..b05b9c6
--- /dev/null
+++ b/plugins/context/context.rb-plugin
@@ -0,0 +1,9 @@
+[RB Plugin]
+Loader=python
+Module=context
+IAge=1
+Name=Context Panel
+Description=Show information related to the currently playing artist and song.
+Authors=John Iacona <plate0salad gmail com>
+Copyright=Copyright © 2009 John Iacona
+Website=http://gitorious.org/rhythmbox-context-pane
diff --git a/plugins/context/context/LyricsTab.py b/plugins/context/context/LyricsTab.py
new file mode 100644
index 0000000..69aae77
--- /dev/null
+++ b/plugins/context/context/LyricsTab.py
@@ -0,0 +1,157 @@
+# -*- Mode: python; coding: utf-8; tab-width: 8; indent-tabs-mode: t; -*-
+#
+# 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.
+#
+# The Rhythmbox authors hereby grant permission for non-GPL compatible
+# GStreamer plugins to be used and distributed together with GStreamer
+# and Rhythmbox. This permission is above and beyond the permissions granted
+# by the GPL license by which Rhythmbox is covered. If you modify this code
+# you may extend this exception to your version of the code, but you are not
+# obligated to do so. If you do not wish to do so, delete this exception
+# statement from your 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 rb, rhythmdb
+import gtk, gobject
+import urllib
+import re, os
+import cgi
+from mako.template import Template
+
+class LyricsTab (gobject.GObject):
+    
+    __gsignals__ = {
+        'switch-tab' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE,
+                                (gobject.TYPE_STRING,))
+    }
+
+    def __init__ (self, shell, toolbar, ds, view):
+        gobject.GObject.__init__ (self)
+        self.shell      = shell
+        self.sp         = shell.get_player ()
+        self.db         = shell.get_property ('db') 
+        self.toolbar    = toolbar
+
+        self.button     = gtk.ToggleButton (_("Lyrics"))
+        self.datasource = ds
+        self.view       = view
+        
+        self.button.show()
+        self.button.set_relief( gtk.RELIEF_NONE ) 
+        self.button.set_focus_on_click(False)
+        self.button.connect ('clicked', 
+            lambda button: self.emit('switch-tab', 'lyrics'))
+        toolbar.pack_start (self.button, True, True)
+
+    def activate (self):
+        print "activating Lyrics Tab"
+        self.button.set_active(True)
+        self.reload ()
+
+    def deactivate (self):
+        print "deactivating Lyrics Tab"
+        self.button.set_active(False)
+
+    def reload (self):
+        entry = self.sp.get_playing_entry ()
+        if entry is None:
+            return
+        self.datasource.fetch_lyrics (entry)
+        self.view.loading (self.datasource.get_artist(), self.datasource.get_title())
+
+class LyricsView (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    = ""
+        self.basepath = 'file://' + os.path.split(plugin.find_file('AlbumTab.py'))[0]
+
+        self.load_tmpl ()
+        self.connect_signals ()
+
+    def connect_signals (self):
+        self.ds.connect ('lyrics-ready', self.lyrics_ready)
+
+    def load_view (self):
+        self.webview.load_string (self.file, 'text/html', 'utf-8', self.basepath)
+
+    def loading (self, current_artist, song):
+        self.loading_file = self.loading_template.render (
+            artist   = current_artist,
+            info     = _("Loading lyrics for %s by %s") % (song, current_artist),
+            song     = song,
+            basepath = self.basepath)
+        self.webview.load_string (self.loading_file, 'text/html', 'utf-8', self.basepath)
+        print "loading screen loaded"
+
+    def load_tmpl (self):
+        self.path = self.plugin.find_file('tmpl/lyrics-tmpl.html')
+        self.loading_path = self.plugin.find_file ('tmpl/loading.html')
+        self.template = Template (filename = self.path, 
+                                  module_directory = '/tmp/context/')
+        self.loading_template = Template (filename = self.loading_path, 
+                                          module_directory = '/tmp/context')
+        self.styles = self.basepath + '/tmpl/main.css'
+
+    def lyrics_ready (self, ds, entry, lyrics):
+        print "loading lyrics into webview"
+        if lyrics is None:
+            lyrics = _("Lyrics not found")
+        else:
+            lyrics = lyrics.strip()
+            lyrics = cgi.escape (lyrics, True)
+            lyrics = lyrics.replace ('\n', '<br />')
+
+        # should include data source information here, but the lyrics plugin
+        # doesn't expose that.
+        self.file = self.template.render (artist     = ds.get_artist (),
+                                          song       = ds.get_title (),
+                                          lyrics     = lyrics,
+                                          stylesheet = self.styles)
+        self.load_view ()
+
+class LyricsDataSource (gobject.GObject):
+    
+    __gsignals__ = {
+        'lyrics-ready' : (gobject.SIGNAL_RUN_LAST, gobject.TYPE_NONE, (rhythmdb.Entry, gobject.TYPE_STRING,)),
+    }
+
+    def __init__ (self, db):
+        gobject.GObject.__init__ (self)
+        self.db = db
+        self.db.connect ('entry-extra-metadata-notify::rb:lyrics', self.lyrics_notify)
+
+    def lyrics_notify (self, db, entry, field, metadata):
+        if entry == self.entry:
+            self.emit ('lyrics-ready', self.entry, metadata)
+
+    def fetch_lyrics (self, entry):
+        self.entry = entry
+        lyrics = self.db.entry_request_extra_metadata(entry, "rb:lyrics")
+        # it never responds synchronously at present, but maybe some day it will
+        if lyrics is not None:
+            self.emit ('lyrics-ready', self.entry, lyrics)
+
+    def get_title (self):
+        return self.db.entry_get(self.entry, rhythmdb.PROP_TITLE)
+
+    def get_artist (self):
+        return self.db.entry_get(self.entry, rhythmdb.PROP_ARTIST)
+
diff --git a/plugins/context/context/Makefile.am b/plugins/context/context/Makefile.am
new file mode 100644
index 0000000..8ae1ebe
--- /dev/null
+++ b/plugins/context/context/Makefile.am
@@ -0,0 +1,11 @@
+# Context Panel Python Plugin
+
+plugindir = $(PLUGINDIR)/context
+plugin_PYTHON = 			\
+	AlbumTab.py			\
+	ArtistTab.py			\
+	ContextView.py			\
+	LastFM.py			\
+	LyricsTab.py			\
+	__init__.py
+
diff --git a/plugins/context/img/spinner.gif b/plugins/context/img/spinner.gif
new file mode 100644
index 0000000..c22262d
Binary files /dev/null and b/plugins/context/img/spinner.gif differ
diff --git a/plugins/context/tmpl/album-tmpl.html b/plugins/context/tmpl/album-tmpl.html
new file mode 100644
index 0000000..05bcbc0
--- /dev/null
+++ b/plugins/context/tmpl/album-tmpl.html
@@ -0,0 +1,75 @@
+<%page args="error, list, artist, stylesheet" />
+<html> <head> <meta http-equiv="content-type" content="text-html; charset=utf-8">
+<%!
+    import re
+    def cleanup(text) :
+        return re.sub(r'\([^\)]*\)', '', text)
+    def sec2hms(time) :
+        hr = 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)
+%>  
+<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');
+        swapText('btn_'+element, 'Hide all tracks', 'Show all tracks');
+    }
+</script>
+</head>
+<body>
+<%  
+    num_albums = min(8, len(list))
+%>
+%if error is None :
+    <h1>Top albums by <em>${artist}</em></h1>
+%for i, entry in enumerate(list) :
+    <%if 'tracklist' not in entry or len(entry['tracklist']) == 0 : continue %>
+    <%if i == num_albums : break %>
+    <div id="album${entry['id']}" class="album">
+    <img width="64" src="${entry['images'][1]}" alt="${entry['images']}"/>
+    <h2>${entry['title']}</h2>
+    %if 'duration' in entry :
+    <% 
+        album_time = sec2hms(entry['duration'])
+        tracks = len(entry['tracklist'])
+    %>
+    <p class="duration">${album_time} (${tracks} tracks)</p>
+    %endif
+    %if 'tracklist' in entry :
+    <% btn_name = "btn_%s" % entry['id'] %>
+    <button id="btn_${entry['id']}"onclick="toggle_vis(${entry['id']})">
+    Show all tracks
+    </button>
+    <table class="hidden" id="${entry['id']}">
+        %for num, title, time in entry['tracklist'] :
+            <% 
+                time = sec2hms(time)
+                title = cleanup(title)
+                num = num+1
+            %>
+            <tr><td>${num}</td><td>${title}</td><td>${time}</td></tr>
+        %endfor
+    </table>
+    %else :
+    <p>Tracklist not available</p>
+    %endif
+    </div>
+%endfor
+%else :
+    <h1>Last.fm Error:</h1>
+    <p class="error">${error}</p>
+%endif
+</body>
+</html>
diff --git a/plugins/context/tmpl/artist-tmpl.html b/plugins/context/tmpl/artist-tmpl.html
new file mode 100644
index 0000000..53147cb
--- /dev/null
+++ b/plugins/context/tmpl/artist-tmpl.html
@@ -0,0 +1,45 @@
+<%page args="artist, image, shortbio, fullbio, stylesheet" />
+<%!
+    import re
+    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" />
+<script language="javascript">
+    function swapClass (element, klass1, klass2) {
+        elt = document.getElementById(element);
+        elt.className = (elt.className == klass1) ? klass2 : klass1;
+    }
+</script>
+</head>
+<body class="artist">
+<h1>${artist}</h1>
+<img src="${image}" />
+<div id="shortbio" class="shown">
+<% shortbio = cleanup(shortbio) %>
+<button name="more" onclick="swapClass('shortbio', 'shown', 'hidden');swapClass('fullbio', 'shown', 'hidden')" />
+Read more
+</button>
+<p>${shortbio}</p>
+</div>
+<div id="fullbio" class="hidden">
+<% fullbio = cleanup(fullbio) %>
+<button name="more" onclick="swapClass('shortbio', 'shown', 'hidden');swapClass('fullbio', 'shown', 'hidden')" />
+Read less
+</button>
+<p>${fullbio}</p>
+<button name="more" onclick="swapClass('shortbio', 'shown', 'hidden');swapClass('fullbio', 'shown', 'hidden')" />
+Read less
+</button>
+</div>
+</body>
+</html>
diff --git a/plugins/context/tmpl/loading.html b/plugins/context/tmpl/loading.html
new file mode 100644
index 0000000..05233ef
--- /dev/null
+++ b/plugins/context/tmpl/loading.html
@@ -0,0 +1,14 @@
+<html>
+<head>
+<meta http-equiv="content-type" content="text-html; charset=utf-8">
+<style type="text/css">
+body { font-size: 8pt; padding: 6px; line-height: 1.4em }
+h1 { font-size: 130% }
+img { display: block; margin-left: auto; margin-right: auto }
+</style>
+</head>
+<body>
+<h1>Loading ${info} for - ${artist} ${song}</h1>
+<img src="${basepath}/img/spinner.gif" />
+</body>
+</html>
diff --git a/plugins/context/tmpl/lyrics-tmpl.html b/plugins/context/tmpl/lyrics-tmpl.html
new file mode 100644
index 0000000..5c5bfd7
--- /dev/null
+++ b/plugins/context/tmpl/lyrics-tmpl.html
@@ -0,0 +1,10 @@
+<html>
+<head>
+<meta http-equiv="content-type" content="text-html; charset=utf-8">
+<link rel="stylesheet" href="${stylesheet}" type="text/css" />
+</head>
+<body>
+<h1>${artist}: <em>${song}</em></h1>
+<p>${lyrics}</p>
+</body>
+</html>
diff --git a/plugins/context/tmpl/main.css b/plugins/context/tmpl/main.css
new file mode 100644
index 0000000..29c5f6f
--- /dev/null
+++ b/plugins/context/tmpl/main.css
@@ -0,0 +1,13 @@
+body { font-size: 8pt; 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; }



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]