This will explain how to get the lyrics of the currently playing song on Amarok on your desktop.
To get started first we need to look for a nice(huge) online lyrics library. After some googling I landed on leoslyrics.com which provides a nice API to search and retrieve lyrics through Artist and Title information. [Incidentally this API is also the one RhythmBox uses to retrieve lyrics]. It also provides the lyrics in a XML so that parsing it is even simpler. So I used ID3 (a Python library to obtain/modify ID3 tags of mp3 files) to get the Artist and Title information and build the URL to query the lyrics database. The returned information is an XML file which is nicely parsed by BeautifulStoneSoup (BeautifulSoup for XML) to get the lyrics. The code so far looks like this:
import sys, os
from urllib import urlopen, quote
import ID3
from BeautifulSoup import BeautifulStoneSoup
import unicodedata
def makeQuery(filepath) :
ob = ID3.ID3(filepath)
ob.setup_dict()
if ob.has_key('ARTIST') and ob.has_key('TITLE') :
return (ob['ARTIST'],ob['TITLE'])
print 'No ID3 Tags available for the file', filepath
sys.exit()
def parseXML(XML) :
soup, info = BeautifulStoneSoup(XML), {}
info['title'] = soup.lyric.title.contents[0]
info['artist'] = soup.lyric.artist.nameTag.contents[0]
info['album'] = soup.lyric.album.nameTag.contents[0]
info['url'] = soup.lyric.album.imageurl.contents[0].strip()
info['lyrics'] = '\n'.join([(i[0].title() + i[1:]).strip() if i else i for i in soup.lyric.text.contents[0].replace('
', '').splitlines()])
for i in info :
try :
info[i] = info[i].normalize('NFKD', title).encode('ascii','ignore')
except :
try :
info[i] = info[i].encode('ascii', 'ignore')
except :
pass
return info
def process(filepath) :
tags = makeQuery(os.path.abspath(filepath))
url = 'http://api.leoslyrics.com/api_search.php?auth=duane' + '&artist=' + quote(tags[0]) + '&songtitle=' + quote(tags[1])
XML = urlopen(url, proxies={'http' : 'http://144.16.192.245:8080'}).read()
soup, ID = BeautifulStoneSoup(XML), None
for i in soup.findAll('result') :
try :
if i['exactmatch'] == 'true' :
ID = i['id']
except KeyError :
pass
if not ID :
print 'No search results for', tags[1], 'by', tags[0]
sys.exit()
url = 'http://api.leoslyrics.com/api_lyrics.php?auth=duane&id=' + ID
info = parseXML(urlopen(url, proxies={'http' : 'http://144.16.192.245:8080'}).read())
return info
def prettyPrint(info) :
return ('Title : %s\nAlbum : %s\nArtist : %s\nCover : %s\n' % (info['title'], info['album'], info['artist'], info['url'] if info['url'] else 'N/A')) \
+ ('\n%s\n%s\n%s' % ('Lyrics'.center(30, '='), info['lyrics'], '='*30))
if __name__ == '__main__' :
args = sys.argv[1:]
if not len(args) :
print 'Specify filepath'
sys.exit()
elif not os.path.isfile(args[0]) :
print 'Please specify valid filepath'
sys.exit()
print prettyPrint(process(args[0]))
#Usage: $ python lyric_finder.py ~/Music/Artist/Album/Song.mp3
Once we are able to get the lyrics for a particular song we need to tie it up with Amarok to fetch the current song's lyrics. Now is a good time to make use of the dcop server to find out whats playing in Amarok. The query goes like this:
$ python lyric_finder.py "`dcop amarok player path`"
With bash we are saved from making Amarok Integration a feature of the script. Instead it is external to it. So the feature is really courtesy of bash and dcop.
Now to the part where we need to get the lyrics on the desktop. Conky is by far the most used/abused program in Linux for this. Conky also goes easy on system resources. A simple conkyrc file for displaying the lyrics on the desktop:
alignment top_left
background yes
border_width 1
cpu_avg_samples 2
default_color steel blue
default_outline_color white
default_shade_color black
double_buffer yes
draw_borders no
draw_graph_borders yes
draw_outline no
draw_shades yes
gap_x 10
gap_y 10
no_buffers yes
maximum_width 500
max_port_monitor_connections 64
max_specials 512
max_user_text 26384
minimum_size 1000 500
net_avg_samples 2
no_buffers yes
out_to_console no
own_window_colour black
own_window_hints undecorated,below,skip_taskbar,sticky,skip_pager
own_window_transparent yes
own_window_type normal
own_window yes
stippled_borders 6
update_interval 5
uppercase no
use_spacer none
use_xft yes
xftfont Proggy#74037CSquareTT:size=14
#xftfont Bitstream Vera Sans Mono:size=9
#xftfont Purisa:size=11
TEXT
${font Purisa:size=24}${if_running amarokapp}${color #0077FF} Now on Amarok${color #6E9DFF}${voffset -25}
${font ProggySquareTT:size=14}${exec python ~/lyric_finder/lyric_finder.py "`dcop amarok player path`"}
${color #0077FF}Last Updated at ${time %H:%M:%S}
${else}${color #0077FF} Amarok Inactive
${endif}
We want a small fixed width font for the lyrics so that it doesnt quickly fill up the screen. ProggySquareTT fits this perfectly, You might either want to change the specified font or download it (in case you dont have it).
It appears we are done. But the truth is, only nearly so. There's a catch. Conky refreshes every update_interval seconds which means it keeps calling the script periodically while the song is playing and re-fetches the lyrics from teh internet every time. This simultaneously hogs on your bandwidth and also increases CPU usage. So we need some kind of intelligent caching to save the lyrics on the hard drive and goto the internet only when its not available. To tell if the version in the HD is the current song's lyrics or not we create a simple SHA256 hash from the song's title and artist for comparison. This way we download and cache the new song's lyrics only once. Subsequent fetching is done from the HD. We create a small adaptor Python script to solve this issue that calls lyric_finder.py only when needed.
import lyric_finder as lf
import sys, os
import hashlib
hashfile = '/tmp/lyric_finder_hash'
lyricfile = '/tmp/lyric_finder_lyrics'
def hashed(tags) :
return hashlib.sha256(tags[0] + tags[1]).hexdigest()
def update(filepath) :
open(hashfile, 'w').write(hashed(lf.makeQuery(filepath)))
lyrics = lf.prettyPrint(lf.process(filepath))
open(lyricfile, 'w').write(lyrics)
print lyrics
if __name__ == '__main__' :
args = sys.argv[1:]
if not len(args) :
print 'Specify filepath'
sys.exit()
elif not os.path.isfile(args[0]) :
print 'Please specify valid filepath'
sys.exit()
tags = lf.makeQuery(args[0])
if not os.path.isfile(hashfile) :
update(args[0])
else :
if open(hashfile).read() == hashed(tags) :
print open(lyricfile).read()
else :
update(args[0])
With a small change in the conkyrc file to call the new adaptor.py we are ready to roll!
PS: It looks very pretty, I know.