Tuesday, June 7, 2011

Small Note

This notice was due nearly two years, but I thought better late than never.

I don't continue to blog here (because I have learnt python since), but have moved here, where I now blog (about non-pythonic things generally).

This small blog was simply there to post interesting things that I did with python at the time, and it is somewhat of a pleasant surprise that one post of this blog turns up as the first hit for a relevant goole search query. What more could I have hoped for? :)

That's all!

Thursday, November 6, 2008

the greatest music player ever!

cmus: The last word on music players



And yes this is after I have tried winamp, windows media player, itunes, amarok(!), mocp, mp3blaster,... So possibly the one that gave the toughest competition was Amarok.
Of course cmus cant do album art, nor can it show you fancy wiki pages and lyrics. But seriously, how many times have you used those features? Music is where it's supposed to belong - the background. If you keep babysitting it and fiddling with the wiki pages, well, thats not the way I want it at least. It doesn't have playcount and smart playlists too. But the last.fm statistics submission entirely takes care of that.

Now lets get to what it can do.


* Cmus is fast! Sexy fast! I won't even bother comparing it with any other player.
* Search (even supports Regular Expressions)
* Filter tracks as simply as :mark duration>300&genre="*rock*"
* Queues and Playlists and a file browser too.
* Vi bindings! Vi style command line!
* It can run on GNU/Screen! ^A^A change song ^A^A back to work :D
* Complete customization - Every keybinding can be customized. And the same for the interface, behaviour,..
* Based on ncurses. Yes this is a feature. More room for tracks (see screenshot) and MUCH more cleaner interface compared to any GUI equivalent.
* Tree view to browse the library
* Smallest memory footprint ever.
* No start up time. (sqlite databases in Amarok are sluggish)
* Stable. (which is something of a non-feature in Amarok)

And of course it looks better. (The colors in the screenshot are from a custom theme I made. Look here if you want it)
And it has nothing to do with python though :D

Monday, September 22, 2008

Now Playing lyrics on your Linux Desktop

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.