Saturday, March 15, 2008

Amarok Control

I've been using RatPoison on my laptop for a while, and even though switching between windows is more convenient than with a mouse, it's generally easier to not switch at all. However, I listen to a lot of music, and I often want to skip tracks, pause,and so on; so I wrote a small collection of scripts to do just that. Unfortunately, querying doesn't work when Unicode strings are passed to it, due to PyDCOP.

amarok_dcop.py

The layer over PyDCOP, designed specifically for Amarok.

#!/usr/bin/python
import pydcop
import sys
import re
import os

def escape(string):
ret = u''
for i in string:
if i == '\'':
ret += '\\\''
elif i == '\\':
ret += '\\\\'
else:
ret += i
return ret

class amarok_dcop(object):
"A remote control class for Amarok using DCOP."
mins = re.compile("^([0-9]+):([0-9]+)$")
number = re.compile("^([0-9]+)$")
amarok = False
repeatmodes = ['off', 'track', 'album', 'playlist']
def __init__(self, amarok = False):
self.amarok = amarok
def play(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.play()
def pause(self, amarok = amarok, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.pause()
def stop(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.stop()
def next(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.next()
def prev(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.prev()
def setRepeat(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
mode = str(args[0])
if(mode == self.repeatmodes[0]):
amarok.player.enableRepeatTrack(False)
elif(mode == self.repeatmodes[1]):
amarok.player.enableRepeatTrack(True)
elif(mode == self.repeatmodes[3]):
amarok.player.enableRepeatPlaylist(True)
def repeat(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
if(amarok.player.repeatTrackStatus()):
return self.repeatmodes[1]
elif(amarok.player.repeatPlaylistStatus()):
return self.repeatmodes[3]
else:
return self.repeatmodes[0]
def seek(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
time = str(args[0])
m = self.mins.match(time)
if(m and int(m.group(2)) < 60):
amarok.player.seek(60 * int(m.group(1)) + int(m.group(2)))
elif(self.number.match(time)):
amarok.player.seek(int(time))
else:
print "Invalid seek time; format is either seconds or minutes:seconds"
def time(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
time = int(amarok.player.trackCurrentTime())
mins = time / 60
secs = time % 60
if(mins):
print "%d:%d" % (mins, secs)
else:
print secs
def volumeUp(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.volumeUp()
def volumeDown(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.volumeDown()
def setVolume(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
amarok.player.setVolume(int(args[0]))
def getVolume(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.getVolume()
def title(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.title()
def artist(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.artist()
def album(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.album()
def year(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.year()
def status(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.player.status()
def details(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
status = amarok.player.status()
if(status):
return "%s - %s (%s, %s)%s" % (amarok.player.title(), amarok.player.artist(), amarok.player.album(), amarok.player.year(), (status == 1) and " (paused)" or "")
else:
return "Not playing"
def playlistTrack(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.playlist.getActiveIndex()
def playlistCount(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
return amarok.playlist.getTotalTrackCount()
def removeTrack(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
if(len(args)):
amarok.playlist.removeByIndex(int(args[0]))
else:
amarok.playlist.removeCurrentTrack()
class QList(list):
def __str__(self):
ret = ''
for i in range(0, len(self)):
print self[i], self[i].encode('utf-8')
ret += self[i].encode('utf-8')
if i < len(self) - 1:
ret += ' AND '
return ret
def fquery(self, amarok = False, title = None, artist = None, album = None):
if(not amarok):
amarok = self.amarok
base = "SELECT tags.url FROM tags INNER JOIN artist ON artist.id = tags.artist LEFT OUTER JOIN album ON album.id = tags.album LEFT OUTER JOIN year ON year.id = tags.year"
supp = self.QList()
if title:
supp += ["tags.title = '%s'" % (escape(title),)]
if artist:
supp += ["artist.name = '%s'" % (escape(artist),)]
if album:
supp += ["album.name = '%s'" % (escape(album),)]
if len(supp):
base += ' WHERE ' + str(supp)
print base
ret = amarok.collection.query(base)
print ret
return ret
def query(self, amarok = False, title = None, artist = None, album = None):
if(not amarok):
amarok = self.amarok
base = u"SELECT artist.name, tags.title, album.name, tags.track, tags.length, year.name, tags.url FROM tags INNER JOIN artist ON artist.id = tags.artist LEFT OUTER JOIN album ON album.id = tags.album LEFT OUTER JOIN year ON year.id = tags.year"
supp = self.QList()
if title:
supp += [u"tags.title = '%s'" % (escape(title),)]
if artist:
supp += [u"artist.name = '%s'" % (escape(artist),)]
if album:
supp += [u"album.name = '%s'" % (escape(album),)]
if len(supp):
base += u' WHERE ' + str(supp)
print base
#ret = amarok.collection.query(base)
return ret
def addFile(self, amarok = False, args = []):
if(not amarok):
amarok = self.amarok
basedir = args[1]
file = args[0]
if file[0:2] == './':
file = file[2:len(file)]
file = os.path.join(basedir, file)
pid = os.fork()
cmd = (u'dcop', u'amarok', u'playlist', u'addMedia', file)
for i in cmd:
print i
if not pid:
os.execvp('dcop', cmd)
else:
os.waitpid(pid, 0)
#amarok.playlist.addMedia(file)

amarok-control.py

The command-line client.

import pydcop
import sys
import re
from amarok_dcop import amarok_dcop





amarok = pydcop.anyAppCalled('amarok')
if(amarok):
control = amarok_dcop(amarok)
commands = {
'play' : [control.play, 0],
'pause' : [control.pause, 0],
'stop' : [control.stop, 0],
'next' : [control.next, 0],
'prev' : [control.prev, 0],
'seek' : [control.seek, 1, "[ seconds | minutes:seconds ]"],
'time' : [control.time, 0],
'volumeUp' : [control.volumeUp, 0],
'volumeDown' : [control.volumeDown, 0],
'title' : [control.title, 0],
'artist' : [control.artist, 0],
'album' : [control.album, 0],
'year' : [control.year, 0],
'details' : [control.details, 0],
'playlistTrack' : [control.playlistTrack, 0],
'playlistCount' : [control.playlistCount, 0]
}
if(len(sys.argv) > 1 and commands.has_key(sys.argv[1])):
cmd = commands[sys.argv[1]]
if(cmd[1] > 0):
args = sys.argv[:]
args.pop(0)
args.pop(0)
if(len(args) >= cmd[1]):
retd = cmd[0](args = args)
if(retd and len(str(retd))):
print str(retd)
else:
if(len(cmd) > 2):
print "Usage:", sys.argv[0], cmd[2]
else:
print "Need", cmd[1], "arguments."
else:
retd = cmd[0]()
if(retd and len(str(retd))):
print str(retd)
else:
listing = '[ '
for i in commands:
listing += i + ' '
if(i == commands.keys()[-1]):
listing += ']'
else:
listing += '| '
print "Use:", sys.argv[0], listing
else:
print 'Amarok not running'

amarok-gtk.py

The window-GUI client.

import pydcop
import sys
import re
import os
from amarok_dcop import amarok_dcop
import pygtk
pygtk.require('2.0')
import gtk


class ControlWindow(object):
control = False
playlists = []
playlistdir = os.getenv('HOME') + '/.kde/share/apps/amarok/playlists'
if os.access(playlistdir, os.F_OK | os.X_OK):
print 'Amarok playlist directory found:', playlistdir
playlists = os.listdir(playlistdir)
def play(self, widget, event, data=None):
if(self.control.status() == 2):
self.control.pause()
widget.set_label('>')
else:
self.control.play()
widget.set_label('||')
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def prev(self, widget, event, data=None):
self.control.prev()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def get_title(self):
if pydcop.anyAppCalled('amarok') and self.control.status():
return control.details()
else:
return 'Amarok Remote'
def next(self, widget, event, data=None):
self.control.next()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def del2next(self, widget, event, data=None):
track = self.control.playlistTrack();
self.control.next()
self.control.removeTrack(args = [track])
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def volumeUp(self, widget, event, data=None):
self.control.volumeUp()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def volumeDown(self, widget, event, data=None):
self.control.volumeDown()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def setVolume(self, widget, event, data = None):
self.control.setVolume(args = [int(widget.get_value())])
self.window.set_title(self.get_title())
def repeat(self, widget, event, data=None):
if(widget.get_active()):
self.control.setRepeat(args = ['track']);
else:
self.control.setRepeat(args = ['playlist']);
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def seek0(self, widget, event, data=None):
title = self.control.title()
self.control.pause()
self.control.seek(args = [1])
self.control.play()
if title != self.control.title():
self.control.prev()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def delete_event(self, widget, event, data=None):
return False
def destroy(self, widget, data=None):
gtk.main_quit()
def __init__(self, control = False):
self.control = control
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("delete_event", self.delete_event)
self.window.connect("destroy", self.destroy)
self.window.set_border_width(4)
self.window.set_title(self.get_title())

self.topbox = gtk.VBox(True, 1)
self.topbox.set_homogeneous(False)
self.table = gtk.Table(3, 4, True)

self.playbutton = gtk.Button('>')
if(self.control.status() == 2):
self.playbutton.set_label('||')
self.playbutton.connect("clicked", self.play, None)
self.table.attach(self.playbutton, 1, 2, 1, 2)
self.playbutton.show()

self.button = gtk.Button('[rm]')
self.button.connect("clicked", self.del2next, None)
self.table.attach(self.button, 0, 1, 0, 1)
self.button.show()

self.button = gtk.Button('<<')
self.button.connect("clicked", self.prev, None)
self.table.attach(self.button, 0, 1, 1, 2)
self.button.show()

self.button = gtk.Button('|<')
self.button.connect("clicked", self.seek0, None)
self.table.attach(self.button, 0, 1, 2, 3)
self.button.show()

self.button = gtk.Button('>>')
self.button.connect("clicked", self.next, None)
self.table.attach(self.button, 2, 3, 1, 2)
self.button.show()

self.button = gtk.Button('V+')
self.button.connect("clicked", self.volumeUp, None)
self.table.attach(self.button, 1, 2, 0, 1)
self.button.show()

self.button = gtk.Button('V-')
self.button.connect("clicked", self.volumeDown, None)
self.table.attach(self.button, 1, 2, 2, 3)
self.button.show()

self.repeatTrack = gtk.CheckButton('Rep.')
self.repeatTrack.show()
if(self.control.repeat() == 'track'):
self.repeatTrack.set_active(True)
self.repeatTrack.connect("toggled", self.repeat, None)
self.table.attach(self.repeatTrack, 2, 3, 2, 3)

self.volume = gtk.VScale()
self.volume.set_update_policy(gtk.UPDATE_CONTINUOUS)
self.volume.set_range(0, 100)
self.volume.set_inverted(True)
self.volume.set_draw_value(False)
self.volume.set_value(self.control.getVolume())
self.volume.connect("value-changed", self.setVolume, None)
self.table.attach(self.volume, 3, 4, 0, 3)
self.volume.show()

self.topbox.pack_start(self.table, False, False, 4)
self.table.show()

self.quit = gtk.Button('X')
self.quit.connect_object("clicked", gtk.Widget.destroy, self.window)
self.topbox.pack_start(self.quit, False, False, 10)
self.quit.show()

self.window.add(self.topbox)
self.topbox.show()

self.window.show()
def main(self):
gtk.main()


amarok = pydcop.anyAppCalled('amarok')
if(amarok):
control = amarok_dcop(amarok)
commands = {
'play' : [control.play, 0],
'pause' : [control.pause, 0],
'stop' : [control.stop, 0],
'next' : [control.next, 0],
'prev' : [control.prev, 0],
'seek' : [control.seek, 1, "[ seconds | minutes:seconds ]"],
'time' : [control.time, 0],
'volumeUp' : [control.volumeUp, 0],
'volumeDown' : [control.volumeDown, 0],
'title' : [control.title, 0],
'artist' : [control.artist, 0],
'album' : [control.album, 0],
'year' : [control.year, 0],
'details' : [control.details, 0]
}
window = ControlWindow(control)
window.main()
else:
print 'Amarok not running'

amarok-bar-gtk.py

The bar-GUI client.

import pydcop
import sys
import re
import os
from amarok_dcop import amarok_dcop
import pygtk
pygtk.require('2.0')
import gtk


class ControlWindow(object):
control = False
playlists = []
playlistdir = os.getenv('HOME') + '/.kde/share/apps/amarok/playlists'
if os.access(playlistdir, os.F_OK | os.X_OK):
print 'Amarok playlist directory found:', playlistdir
playlists = os.listdir(playlistdir)
def play(self, widget, event, data=None):
if(self.control.status() == 2):
self.control.pause()
widget.set_label('>')
else:
self.control.play()
widget.set_label('||')
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def prev(self, widget, event, data=None):
self.control.prev()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def get_title(self):
if pydcop.anyAppCalled('amarok') and self.control.status():
return control.details()
else:
return 'Amarok Remote'
def next(self, widget, event, data=None):
self.control.next()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def del2next(self, widget, event, data=None):
track = self.control.playlistTrack();
self.control.next()
self.control.removeTrack(args = [track])
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def volumeUp(self, widget, event, data=None):
self.control.volumeUp()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def volumeDown(self, widget, event, data=None):
self.control.volumeDown()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def setVolume(self, widget, event, data = None):
self.control.setVolume(args = [int(widget.get_value())])
self.window.set_title(self.get_title())
def repeat(self, widget, event, data=None):
if(widget.get_active()):
self.control.setRepeat(args = ['track']);
else:
self.control.setRepeat(args = ['playlist']);
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def seek0(self, widget, event, data=None):
title = self.control.title()
self.control.pause()
self.control.seek(args = [1])
self.control.play()
if title != self.control.title():
self.control.prev()
self.window.set_title(self.get_title())
self.volume.set_value(self.control.getVolume())
def delete_event(self, widget, event, data=None):
return False
def destroy(self, widget, data=None):
gtk.main_quit()
def __init__(self, control = False):
self.control = control
self.window = gtk.Window(gtk.WINDOW_TOPLEVEL)
self.window.connect("delete_event", self.delete_event)
self.window.connect("destroy", self.destroy)
self.window.set_border_width(4)
self.window.set_title(self.get_title())

self.topbox = gtk.HBox(True, 1)
self.topbox.set_homogeneous(False)

self.button = gtk.Button('[rm]')
self.button.connect("clicked", self.del2next, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.button = gtk.Button('<<')
self.button.connect("clicked", self.prev, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.button = gtk.Button('|<')
self.button.connect("clicked", self.seek0, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.playbutton = gtk.Button('>')
if(self.control.status() == 2):
self.playbutton.set_label('||')
self.playbutton.connect("clicked", self.play, None)
self.topbox.pack_start(self.playbutton, True, True, 1)
self.playbutton.show()

self.button = gtk.Button('>>')
self.button.connect("clicked", self.next, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.repeatTrack = gtk.CheckButton('Rep.')
self.repeatTrack.show()
if(self.control.repeat() == 'track'):
self.repeatTrack.set_active(True)
self.repeatTrack.connect("toggled", self.repeat, None)
self.topbox.pack_start(self.repeatTrack, True, True, 1)

self.button = gtk.Button('V+')
self.button.connect("clicked", self.volumeUp, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.button = gtk.Button('V-')
self.button.connect("clicked", self.volumeDown, None)
self.topbox.pack_start(self.button, True, True, 1)
self.button.show()

self.volume = gtk.HScale()
self.volume.set_update_policy(gtk.UPDATE_CONTINUOUS)
self.volume.set_range(0, 100)
self.volume.set_inverted(False)
self.volume.set_draw_value(False)
self.volume.set_value(self.control.getVolume())
self.volume.connect("value-changed", self.setVolume, None)
self.topbox.pack_start(self.volume, True, True, 1)
size = self.volume.get_size_request()
print size
self.volume.show()

self.quit = gtk.Button('X')
self.quit.connect_object("clicked", gtk.Widget.destroy, self.window)
self.topbox.pack_start(self.quit, True, True, 10)
self.quit.show()

self.window.add(self.topbox)
self.topbox.show()

self.window.show()
def main(self):
gtk.main()


amarok = pydcop.anyAppCalled('amarok')
if(amarok):
control = amarok_dcop(amarok)
commands = {
'play' : [control.play, 0],
'pause' : [control.pause, 0],
'stop' : [control.stop, 0],
'next' : [control.next, 0],
'prev' : [control.prev, 0],
'seek' : [control.seek, 1, "[ seconds | minutes:seconds ]"],
'time' : [control.time, 0],
'volumeUp' : [control.volumeUp, 0],
'volumeDown' : [control.volumeDown, 0],
'title' : [control.title, 0],
'artist' : [control.artist, 0],
'album' : [control.album, 0],
'year' : [control.year, 0],
'details' : [control.details, 0]
}
window = ControlWindow(control)
window.main()
else:
print 'Amarok not running'

I also decided to write something that would automatically update my status in Pidgin based on the song I was playing in Amarok. However, in order to check if Pidgin is running, I have it get a Pidgin DBUS object every five seconds, which causes the script to fail with an exception every half-hour or so. I guess PyDBUS doesn't like to be polled.

amarok-pidgin-status.py

#!/usr/bin/python
import pydcop
import sys
import re
from time import sleep
from amarok_dcop import amarok_dcop
import dbus
import sys
import os
import signal

pidgin = None
cont = True

pidfname = ''
def handler(signal, frame):
status = pidgin.PurpleSavedstatusGetCurrent()
pidgin.PurpleSavedstatusSetMessage(status, '')
pidgin.PurpleSavedstatusActivate(status)
cont = False
if(os.access(pidfname, os.F_OK)):
os.remove(pidfname)
sys.exit()


signal.signal(signal.SIGHUP, handler)
signal.signal(signal.SIGTERM, handler)




if(len(sys.argv)):
session_bus = dbus.SessionBus()
obj = session_bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject")
pidgin = dbus.Interface(obj, "im.pidgin.purple.PurpleInterface")

amarok = pydcop.anyAppCalled('amarok')
if(pidgin and amarok):
control = amarok_dcop(amarok)
details = ''
pid = os.getpid()
pidfname = '/tmp/' + str(os.getlogin()) + '-amarok-pidgin-status.pid'
pidfile = open(pidfname, 'w')
pidfile.write(str(pid))
pidfile.close()
while cont:
if pydcop.anyAppCalled('amarok') and session_bus.get_object("im.pidgin.purple.PurpleService", "/im/pidgin/purple/PurpleObject"):
if control.status():
newdetails = control.details()
if newdetails != details:
details = newdetails
status = pidgin.PurpleSavedstatusGetCurrent()
pidgin.PurpleSavedstatusSetMessage(status, 'Now playing ' + details)
pidgin.PurpleSavedstatusActivate(status)
sleep(5)
else:
cont = False
break
if(os.access(pidfname, os.F_OK)):
os.remove(pidfname)
else:
print "Unable to get Pidgin or Amarok not running."
else:
print "Usage:", sys.argv[0], "account buddy"

No comments: