=== added directory 'src/quodlibet' === added file 'src/quodlibet/Makefile.am' --- src/quodlibet/Makefile.am 1970-01-01 00:00:00 +0000 +++ src/quodlibet/Makefile.am 2008-02-04 17:38:05 +0000 @@ -0,0 +1,9 @@ +SUBDIRS = icons +APPLET_NAME = quodlibet +APPLET_MAIN_FILE = qlapplet.py +include $(top_srcdir)/Makefile.python-applet + +dist_applet_DATA = \ + qlinterface.py \ + imageproducer.py \ + $(NULL) === added directory 'src/quodlibet/icons' === added file 'src/quodlibet/icons/Makefile.am' --- src/quodlibet/icons/Makefile.am 1970-01-01 00:00:00 +0000 +++ src/quodlibet/icons/Makefile.am 2008-02-04 17:38:05 +0000 @@ -0,0 +1,5 @@ +dist_mediaiconplay_DATA = \ + cover.svg \ + top.png + +mediaiconplaydir = $(libdir)/awn/applets/quodlibet/icons === added file 'src/quodlibet/icons/cover.svg' --- src/quodlibet/icons/cover.svg 1970-01-01 00:00:00 +0000 +++ src/quodlibet/icons/cover.svg 2008-02-04 17:38:05 +0000 @@ -0,0 +1,663 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + image/svg+xml + + Missing Album Cover Art + + + + + + + + + cdrom + missing + artwork + inlay + + + + + + Jakub Steiner + + + + + + + + + + + + + + + + + + + + + + + + + + === added file 'src/quodlibet/icons/top.png' Binary files src/quodlibet/icons/top.png 1970-01-01 00:00:00 +0000 and src/quodlibet/icons/top.png 2008-02-04 17:38:05 +0000 differ === added file 'src/quodlibet/imageproducer.py' --- src/quodlibet/imageproducer.py 1970-01-01 00:00:00 +0000 +++ src/quodlibet/imageproducer.py 2008-02-04 17:38:05 +0000 @@ -0,0 +1,101 @@ +#!/usr/bin/env python + +#Taken from Last.fm AWN Applet +# 2007 Jonathan Rauprich +# 2007 Tomas Kramar +# modified: 2008 David Horsley + +import sys +import gtk +import cairo +import os + +class ImageProducer: + + # dimensions of the cover picture + WIDTH = 128.0 + HEIGHT = 112.0 + # look at the image "icons/top.png" + # it is an image of empty cd cover + # the cover image is put in the area between borders + BORDER_TOP = 3.0 + BORDER_LEFT = 18.0 + BORDER_BOTTOM = 8.0 + BORDER_RIGHT = 6.0 + + def __init__(self, height): + self.path = os.path.dirname(__file__) + + self.calculate_cover_dimensions(height) + + def calculate_cover_dimensions(self, height): + #this is needed to make the generated cover fit the height of awn bar + #cover dimensions written in uppercase are default - the borders have been measured + #now we need to scale it, recalculate new borders and width - the height is not calculated - it is the height of awn bar + + scale_factor = height / self.HEIGHT + self.HEIGHT = float(height) # casting it to float is really important due to the way how python division works + self.WIDTH = round(self.WIDTH * scale_factor) + self.BORDER_TOP = round(self.BORDER_TOP * scale_factor) + self.BORDER_LEFT = round(self.BORDER_LEFT * scale_factor) + self.BORDER_BOTTOM = round(self.BORDER_BOTTOM * scale_factor) + self.BORDER_RIGHT = round(self.BORDER_RIGHT * scale_factor) + + + def generate_cover(self, pixbuf): + # first step is to create a surface from jpg image and scale it down + + #pixbuf = gtk.gdk.pixbuf_new_from_file(filename) + + image_width = self.WIDTH - self.BORDER_RIGHT - self.BORDER_LEFT + image_height = self.HEIGHT - self.BORDER_TOP - self.BORDER_BOTTOM + + surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(image_width), int(image_height)) + ctx = cairo.Context(surface) + + x_scale = image_width / pixbuf.get_width() + y_scale = image_height / pixbuf.get_height() + ctx.scale(x_scale,y_scale) + + ct = gtk.gdk.CairoContext(ctx) + ct.set_source_pixbuf(pixbuf,0,0) + ctx.paint() + + # we want to create an effect of cd in a box, + # so the cover go first, and then the cd box on top + f_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, int(self.WIDTH), int(self.HEIGHT)) + f_ctx = cairo.Context(f_surface) + f_ctx.set_source_surface(surface, self.BORDER_LEFT, self.BORDER_TOP) + f_ctx.rectangle(self.BORDER_LEFT, self.BORDER_TOP, self.WIDTH, self.HEIGHT) + + f_ctx.fill() + + base = cairo.ImageSurface.create_from_png(self.path + "/icons/top.png") + x_scale = self.WIDTH / base.get_width() + y_scale = self.HEIGHT / base.get_height() + + f_ctx.scale(x_scale, y_scale) + + #f_ctx.new_path() + f_ctx.set_source_surface(base) + f_ctx.paint() + + return self.get_pixbuf_from_surface(f_surface) + + def get_default_cover(self): + return "icons/default.png" + + # Stolen from diogodivision's "BlingSwitcher" + def get_pixbuf_from_surface(self, surface): + from StringIO import StringIO + sio = StringIO() + surface.write_to_png(sio) + sio.seek(0) + loader = gtk.gdk.PixbufLoader() + loader.write(sio.getvalue()) + loader.close() + return loader.get_pixbuf() + +if __name__ == '__main__': + app = ImageProducer(40) + app.generate_cover("icons/sample_cover.jpg") === added file 'src/quodlibet/qlapplet.py' --- src/quodlibet/qlapplet.py 1970-01-01 00:00:00 +0000 +++ src/quodlibet/qlapplet.py 2008-02-04 17:38:05 +0000 @@ -0,0 +1,206 @@ +#!/usr/bin/python + +#Quod Libet applet or AWN +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA +# +#Author: +# 2008 David Horsley widdma@gmail.com + +import sys, os +import os.path +import awn +import gobject +import pygtk +import gtk +from gtk import gdk +from xml.sax.saxutils import escape +from imageproducer import ImageProducer #from the wonderful last.fm AWN applet + +from qlinterface import QLInterface + +class QLApplet(awn.AppletSimple, QLInterface): + """ + AWN Applet for Quod Libet + """ + + labeltext = """Now Playing: +%(title)s +%(artist)s +%(album)s""" + path = os.path.dirname(__file__) + + def __init__(self, uid, orient, height): + awn.AppletSimple.__init__(self, uid, orient, height) + QLInterface.__init__(self) + + + self.height = height + + self.theme = gtk.IconTheme() + self.hide_window() + + self.title = awn.awn_title_get_default() + + if self.ql_running(): + self.update_title(self.CurrentSong()) + else: + self.title_text = "Player not running (middle click to start)" + #Dialogs + self.__build_main_dialog() + self.__build_not_running_dialog() + self.update_album_art() + + + #Applet Button + self.connect ("button-press-event", self.applet_button_press) + self.connect ("enter-notify-event", lambda widget, event: self.title.show(self, self.title_text)) + self.connect ("leave-notify-event", lambda widget, event: self.title.hide(self)) + + #Events + def applet_button_press(self, widget, event): + if self.ql_running(): + if event.button == 2: + self.toggle_window() + else: + self.main_dialog.show_all() + else: + if event.button == 2: + os.popen2("quodlibet") + else: + self.not_running_dialog.show_all() + + def onQLExited(self): + self.title_text = "Player not running (middle click to start)" + self.update_album_art() + + def onQLStarted(self): + gobject.timeout_add(2000, self.hide_window) + + def onSongStarted(self, song): + self.update_title(song) + self.update_album_art() + + song = self.escape(song) + self.songlabel.set_label(self.labeltext % song) + + def onSongStoped(self, song, stopped): + self.title_text = "Quod Libet" + self.update_album_art() + + + + #Dialog Builders + def __build_not_running_dialog(self): + + self.not_running_dialog = awn.AppletDialog(self) + start_button = gtk.Button() + start_button.set_label("Start") + start_button.connect("clicked", lambda widget: os.popen2("quodlibet")) + + label = gtk.Label("Quod Libet Not Running") + label.set_use_markup(True) + + vbox = gtk.VBox(False, 10) + vbox.pack_start(label) + vbox.pack_start(start_button) + + align = gtk.Alignment() + align.set_padding(10,10,10,10) + align.add(vbox) + + self.not_running_dialog.add(align) + self.not_running_dialog.connect("focus-out-event", lambda widget, event: self.not_running_dialog.hide()) + + def __build_secondry_dialog(self): + self.secondry_dialog = awn.AppletDialog(self) + + + def __build_main_dialog(self): + self.main_dialog = awn.AppletDialog(self) + self.main_dialog.set_title("Quod Libet") + song_box = gtk.VBox(spacing = 5) + button_box = gtk.HBox() + + self.songlabel = gtk.Label() + self.songlabel.set_use_markup(True) + self.songlabel.set_padding(5,0) + song_box.pack_start(self.songlabel) + + if self.ql_running(): + song = self.escape(self.CurrentSong()) + self.songlabel.set_label(self.labeltext % song) + + button_clicked = lambda widget: self.control(widget.action) + + for name in ("previous","play-pause","next"): + button = gtk.ToolButton ("gtk-media-%s"%name.split("-")[0]) + button.connect("clicked", button_clicked) + button_box.pack_start(button, False) + button.action = name + + align = gtk.Alignment(0.5,0.5,0,0) + align.add(button_box) + song_box.pack_start(align, False) + + self.albumArt = gtk.Image() + self.update_album_art() + + album_box = gtk.HBox(False, 10) + album_box.pack_start(song_box) + album_box.pack_start(self.albumArt) + self.main_dialog.add(album_box) + + self.main_dialog.connect("focus-out-event", lambda widget, event: self.main_dialog.hide()) + + def update_title(self, song): + #TODO: make this an option + try: + self.title_text = "%(tracknumber)s: %(title)s - %(artist)s" % song + except: + pass + + def update_album_art(self): + + if self.ql_running(): + try: + pixbuf = gdk.pixbuf_new_from_file(os.path.expanduser("~/.quodlibet/current.cover")) + except: + pixbuf = gdk.pixbuf_new_from_file(self.path + "/icons/cover.svg") + + else: + pixbuf = gdk.pixbuf_new_from_file(self.path + "/icons/cover.svg") + + self.albumArt.set_from_pixbuf(pixbuf.scale_simple(128,128,gtk.gdk.INTERP_HYPER)) + + glossy_album = ImageProducer(128).generate_cover(pixbuf) + if self.height != glossy_album.get_height(): + glossy_album = glossy_album.scale_simple(glossy_album.get_width()*(self.height.__float__() / glossy_album.get_height().__float__()),self.height,gtk.gdk.INTERP_HYPER) + self.set_icon (glossy_album) + + def escape(self, dict_str): + out= {} + for key, value in dict_str.items(): + out[key] = escape(value) + return out + + +if __name__ == "__main__": + awn.init (sys.argv[1:]) + applet = QLApplet(awn.uid, awn.orient, awn.height) + awn.init_applet (applet) + applet.show_all () + gtk.main () === added file 'src/quodlibet/qlinterface.py' --- src/quodlibet/qlinterface.py 1970-01-01 00:00:00 +0000 +++ src/quodlibet/qlinterface.py 2008-02-04 17:38:05 +0000 @@ -0,0 +1,144 @@ +#!/usr/bin/python + +#Quod Libet DBus and FIFO interface wrapper +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library; if not, write to the +# Free Software Foundation, Inc., 59 Temple Place - Suite 330, +# Boston, MA 02111-1307, USA +# +#Author: +# 2008 David Horsley widdma@gmail.com + +import os +import os.path +import tempfile +import dbus +import dbus.mainloop.glib + +class QLInterface(): + """ + An interface to control and retrive infomation from Quodlibet + """ + #QL's DBus interface doesn't suport as many commands as it's FIFO + #But FIFO doesn't have signals, so we use both. + + controlfile = os.path.expanduser("~/.quodlibet/control") + controls = ["next", "previous", "play", "pause", "play-pause", + "hide-window", "show-window", "toggle-window", + "focus", "quit", "unfilter"] + + controls_opt = ["seek", "order", "repeat", "query", "volume", "filter", + "set-rating", "set-browser", "open-browser", "random", + "song-list", "queue", "enqueue", "unqueue"] + + dbus_methods = ["GetPostion", "IsPlaying", "CurrentSong", "Next", + "Previous", "Pause", "Play", "PlayPause"] + + def __init__(self): + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + + self.bus = dbus.SessionBus() + self.bus.add_signal_receiver(self.sig_catch, dbus_interface="org.freedesktop.DBus", signal_name = "NameOwnerChanged") + + if self.ql_running(): + self.setup_dbus() + + def sig_catch(self, name, old, new): + if name == "net.sacredchao.QuodLibet": + if old == "": + self.setup_dbus() + self.onQLStarted() + else: + self.onQLExited() + + def setup_dbus(self): + self.QL = self.bus.get_object("net.sacredchao.QuodLibet", "/net/sacredchao/QuodLibet") + signals = ("SongStarted","SongEnded","Paused","Unpaused") + for signal in signals: + self.QL.connect_to_signal(signal, getattr(self, "on%s" % signal), dbus_interface="net.sacredchao.QuodLibet") + + + def onQLStarted(self): + pass + + def onQLExited(self): + pass + + def onPaused(self): + pass + + def onUnpaused(self): + pass + + def onSongStarted(self, song): + pass + + def onSongEnded(self, song, stop): + pass + + def control(self, action, arg=""): + if self.ql_running(): + if action in self.controls: + try: + f = file(self.controlfile, "w") + f.write(action) + f.close() + except: + pass + elif action in self.controls_opt: + actstr = action + " " + arg + try: + f = file(self.controlfile, "w") + f.write(actstr) + f.close() + except: + pass + + def retrive(command): + #Shamelessly hooked from quodlibet.py + fd, filename = tempfile.mkstemp() + try: + os.unlink(filename) + # mkfifo fails if the file exists, so this is safe. + os.mkfifo(filename, 0600) + f = file(const.CONTROL, "w") + f.write(command + " " + filename) + f.close() + + f = file(filename, "r") + out = f.read() + try: os.unlink(filename) + except EnvironmentError: pass + f.close() + return out + except TypeError: + try: os.unlink(filename) + except EnvironmentError: pass + + + def __getattr__(self,name): #Mostly to prove how cool python is to myself + name = name.replace("_","-") + if name in self.controls: + return lambda: self.control(name) + if name in self.controls_opt: + return lambda x: self.control(name, x) + if name in self.dbus_methods: + return getattr(self.QL, name) + else: + raise AttributeError + + + def ql_running(self): + return ( os.path.exists(self.controlfile) and self.bus.name_has_owner("net.sacredchao.QuodLibet") ) + === added file 'src/quodlibet/quodlibet.desktop.in.in' --- src/quodlibet/quodlibet.desktop.in.in 1970-01-01 00:00:00 +0000 +++ src/quodlibet/quodlibet.desktop.in.in 2008-02-04 17:38:05 +0000 @@ -0,0 +1,9 @@ +[Desktop Entry] +Version=1.0 +Name=Quod Libet Control +Type=Application +Comment=A media-control applet +Exec=quodlibet/qlapplet.py +Icon=quodlibet +X-AWN-AppletType=Python +Name[en_US]=Quod Libet Control === modified file 'configure.ac' --- configure.ac 2008-01-28 21:15:59 +0000 +++ configure.ac 2008-02-04 17:37:53 +0000 @@ -327,6 +327,9 @@ src/quit-applet/Makefile src/quit-applet/icons/Makefile src/quit-applet/quit-applet.desktop.in +src/quodlibet/Makefile +src/quodlibet/icons/Makefile +src/quodlibet/quodlibet.desktop.in src/separator/Makefile src/separator/separator.desktop.in src/shiny-switcher/Makefile