# coding: utf-8 """ Full Screen Music Info ====================== A small program to show fullscreen info about the currently playing song in your favorite music playing program. Currently Amarok (pre KDE4) and SqueezeCenter are supported. (CC) BY SA/GPL2+ - Tormod Ravnanger Landet - http://tormod.landet.net Note: I have not actually bothered to check if the Creative Commons Attribution-Share Alike license is compatible with pygtk and pydbus. This program is almost to short to be copyrightable anyway. Contact me if you want a different license. """ #!/usr/bin/env python import telnetlib, urllib, optparse, sys from subprocess import Popen, PIPE import pygtk pygtk.require('2.0') import gtk, gobject, gtk.gdk as gdk import dbus class AmarokController(object): def _call_amarok(self, part, cmd, arg=''): return Popen("dcop amarok %s %s %s" % (part, cmd, arg), shell=True, stdout=PIPE).stdout.read().strip() is_playing = property(lambda self: self._call_amarok('player', 'isPlaying') == 'true') artist = property(lambda self: self._call_amarok('player', 'artist')) title = property(lambda self: self._call_amarok('player', 'title')) album = property(lambda self: self._call_amarok('player', 'album')) show_osd = property(lambda self: self._call_amarok('player', 'osdEnabled') == True, lambda self, b: self._call_amarok('player', 'enableOSD', b and 1 or 0)) def __str__(self): if self.is_playing: return "Amarok playing: %s - %s -%s" % (self.artist, self.title, self.album) else: return "Amarok: not playing" class SqueezeCenterController(object): def __init__(self): self.tn = telnetlib.Telnet('localhost', 9090) def _ask(self, question): self.tn.write(question+'\n') ans = self.tn.read_until('\n') return urllib.unquote(ans.split()[2]) is_playing = property(lambda self: self._ask('mode ?') == 'play') artist = property(lambda self: self._ask('artist ?')) title = property(lambda self: self._ask('title ?')) album = property(lambda self: self._ask('album ?')) show_osd = property(lambda self: False, lambda self, b: None) def __str__(self): if self.is_playing: return "SqueezeCenter playing: %s - %s -%s" % (self.artist, self.title, self.album) else: return "SqueezeCenter: not playing" class SpotifyController(object): def __init__(self): from Xlib.display import Display display = Display() self.root = display.screen().root self.window = None self.separator = u'–' @property def info(self): if not self.window: windows = self.root.query_tree().children for i, window in enumerate(windows): try: name = window.get_property(39, 0, 0, 200000).value except: continue if name.startswith('Spotify'): self.window = window break if not self.window: return 'ERROR', 'Minimize to the systray to get the window info, you can maximize later' name = self.window.get_property(39, 0, 0, 200000).value data = name.split(self.separator) if len(data) == 1: return False artist = ' '.join(data[0].split(' - ')[1:]).strip() title = data[1].strip() return artist[:-3], title[3:] is_playing = property(lambda self: self.info) artist = property(lambda self: self.info[0]) title = property(lambda self: self.info[1]) album = property(lambda self: '') show_osd = property(lambda self: False, lambda self, b: None) def __str__(self): if self.is_playing: return "Spotify playing: %s - %s -%s" % (self.artist, self.title, self.album) else: return "Spotify not playing" class ScreensaverController(object): cookie = None inhibiter = dbus.Interface(dbus.Bus(dbus.Bus.TYPE_SESSION).get_object('org.gnome.ScreenSaver', '/org/gnome/ScreenSaver'), "org.gnome.ScreenSaver") @classmethod def inhibit(cls): if cls.cookie is None: cls.cookie = cls.inhibiter.Inhibit('Full screen music info', 'Playing tunes') else: print "Warning: inhibiting screensaver when it is already inhibited" @classmethod def un_inhibit(cls): if cls.cookie is not None: cls.inhibiter.UnInhibit(cls.cookie) cls.cookie = None else: print "Warning: un-inhibiting screensaver when it is not inhibited" class MusicOSD(gtk.DrawingArea): def __init__(self, player): super(MusicOSD, self).__init__() self.connect('expose_event', self.expose) self.player = player self.player.show_osd = False gobject.timeout_add(1000, self.update) # Empty cursor pixmap = gtk.gdk.Pixmap(None, 1, 1, 1) color = gtk.gdk.Color() self.empty_cursor = gtk.gdk.Cursor(pixmap, pixmap, color, color, 0, 0) def expose(self, widget, event): context = widget.window.cairo_create() context.rectangle(event.area.x, event.area.y, event.area.width, event.area.height) context.clip() self.draw_osd(context, event.area.width, event.area.height) self.window.set_cursor(self.empty_cursor) return False def update(self): if self.window: alloc = self.get_allocation() rect = gdk.Rectangle(0, 0, alloc.width, alloc.height) self.window.invalidate_rect(rect, True) self.window.process_updates(True) return True def draw_osd(self, cx, width, height): # Set background color cx.set_source_rgb(0.0, 0.0, 0.0) cx.rectangle(0, 0, width, height) cx.fill() cx.paint() # Show info cx.set_source_rgb(1.0, 1.0, 1.0) font_size = height/7.0 line_height = 1.5 def fit_text_to_width(text, width, start_font_size=None, height=None): """ Set the maximum possible font size for displaying a text in a specified width """ if start_font_size is None: fs = width else: fs = start_font_size if height is None: height = fs cx.set_font_size(fs) text_height, text_width = cx.text_extents(text)[3:5] while text_width > width or text_height > height: fs *= 0.95 cx.set_font_size(fs) text_height, text_width = cx.text_extents(text)[3:5] return text_width, text_height, fs def show_header(text): """Show a large header text across the upper half of the screen""" w, h, _ = fit_text_to_width(text, width*0.90, None, height/2.0*0.80) cx.move_to((width-w)/2.0, (height/2.0-h)/2.0+h) cx.show_text(text) def show_centered(text, ypos, fs): """Function to ease drawing of centered text""" cx.save() text_width, text_height, _ = fit_text_to_width(text, width*0.90, fs) cx.move_to((width-text_width)/2.0, ypos+text_height) cx.show_text(text) cx.restore() if self.player.is_playing: artist = self.player.artist title = self.player.title album = self.player.album if artist.lower().strip() == 'various' and '/' in title: artist = title.split('/')[0].strip() title = '/'.join(title.split('/')[1:]).strip() show_header(artist) show_centered(title, height/2.0, font_size) show_centered(album, height/2.0 + font_size*line_height, font_size) else: show_centered("Music is not playing" , height/2.0, font_size) def key_press_impl(win,event): if event.string == 'q': win.player.show_osd = True gtk.main_quit() if event.string == 'f': if not hasattr(win, 'is_fullscreen') or win.is_fullscreen: win.unfullscreen() win.is_fullscreen = False win.player.show_osd = True ScreensaverController.un_inhibit() else: ScreensaverController.inhibit() win.fullscreen() win.is_fullscreen = True win.player.show_osd = False SUPPORTED_PROGRAMS = dict(squeezecenter=SqueezeCenterController, amarok=AmarokController, spotify=SpotifyController) def main(controller_type): window = gtk.Window() player = controller_type() osd = MusicOSD(player) window.add(osd) window.player = player #window.set_property('skip-taskbar-hint', False) window.set_title('Full screen music info') #window.set_app_paintable(True) #window.set_decorated(True) window.set_default_size(500, 500) window.fullscreen() ScreensaverController.inhibit() window.connect('delete-event', gtk.main_quit) window.connect("key_press_event", key_press_impl) window.show_all() gtk.main() if __name__ == '__main__': parser = optparse.OptionParser() parser.add_option("-p", "--program", dest="program", metavar="PROGRAM", help="Your favorite music player") (options, args) = parser.parse_args() program = (options.program or 'Spotify').lower() if not program in SUPPORTED_PROGRAMS: if program: print "ERROR: the program is not supported!" parser.print_help() print "Supported programs:" for prog in SUPPORTED_PROGRAMS: print " -", prog sys.exit(1) main(SUPPORTED_PROGRAMS[program])