After several months thinking about it, and just two requests, I finally decided to publish satyr's code. I decided to use github because I already switched to satyr from hg to git, mainly for testing and understanding it. I think I can live with hg, althought branch management in git seems to be given more thought and a good implementation.

So, without further ado: satyr in github

Remember, it's still a test software, by no means polished or ready for human consumption, and with very low development force. Still, I think it has some nice features, like interchangeable skins and a well defined backend, D-Bus support, quick tag edition, reasonable collection managment, and, thanks to Phonon, almost-gapless playback and things like «stop after playing the current this file» (but not «after any given file» yet).

In Debian Sid it mostly works only with the GStreamer backend; I haven't tried the xine one and I know VLC does not emit a signal needed for queueing the next song, so you have to press «next» after each song. AFAIK this is fixed upstream.

satyr pykde python

Posted Sat 03 Sep 2011 02:59:43 PM CEST Tags: pykde

satyr-0.3.2 "I should install my own food" is out. The Changelog is not very impressive:

  • We can save the list of queued Songs on exit and load it at startup.
  • The queue position is not presented when editing the title of a Song that is queued.
  • Setting the track number works now (was a bug).
  • Fixed a bug in setup.py.

but the last one is somewhat important (Thanks Chipaca). Also, 2 months ago, I made the satyr-0.3.1 "freudian slip" release, when user 'dglent' from http://kde-apps.org/ found a packaging bug. It was not only a bugfixing revision, it also included new features:

  • nowPlaying(), exported via dbus.
  • 'now playing' plugin for irssi.
  • Changes in tags is copied to selected cells (in the same column). This allows 'Massive tag writing'.
  • Fixed "NoneType has no attribute 'row'" bug, but this is just a workaround.
  • Forgot to install complex.ui.

Now go get it!


satyr pykde python

Posted Wed 24 Feb 2010 10:16:41 PM CET Tags: pykde

A couple of days ago Marcelo Fernández wrote a simple image viewer in PyGTK. It's less than 200 lines long[1], and I thought that it would be nice to compare how the same app would be written in PyKDE4. But then I though that it would not be fair, as KDE is a whole desktop environment and GTK is 'only' a widget library, so I did it in PyQt4 instead.

To make this even more fair, I hadn't had a good look at the code itself, I only run it to see what it looks like: a window with only the shown image in it, both scrollbars, no menu or statusbar, and no external file, so I assume he builds the ui 'by hand'. He mentions these features:

  • Pan the image with the mouse.
  • F1 to F5 handle the zoom from 'fit to window', 25%, 50%, 75% and 100%.
  • Zooming with the mouse wheel doesn't work.

Here's my take:

#! /usr/bin/python
# -*- coding: utf-8 -*-

# OurManInToulon - Example image viewer in PyQt4
# Marcos Dione <mdione@grulic.org.ar> - http://grulicueva.homelinux.net/~mdione/glob/

# TODO:
#     * add licence! (GPLv2 or later)

from PyQt4.QtGui import QApplication, QMainWindow, QGraphicsView, QGraphicsScene
from PyQt4.QtGui import QPixmap, QGraphicsPixmapItem, QAction, QKeySequence
import sys

class OMITGraphicsView (QGraphicsView):
    def __init__ (self, pixmap, scene, parent, *args):
        QGraphicsView.__init__ (self, scene)
        self.zoomLevel= 1.0
        self.win= parent
        self.img= pixmap
        self.setupActions ()

    def setupActions (self):
        # a factory to the right!
        zoomfit= QAction (self)
        zoomfit.setShortcuts ([QKeySequence.fromString ('F1')])
        zoomfit.triggered.connect (self.zoomfit)
        self.addAction (zoomfit)

        zoom25= QAction (self)
        zoom25.setShortcuts ([QKeySequence.fromString ('F2')])
        zoom25.triggered.connect (self.zoom25)
        self.addAction (zoom25)

        zoom50= QAction (self)
        zoom50.setShortcuts ([QKeySequence.fromString ('F3')])
        zoom50.triggered.connect (self.zoom50)
        self.addAction (zoom50)

        zoom75= QAction (self)
        zoom75.setShortcuts ([QKeySequence.fromString ('F4')])
        zoom75.triggered.connect (self.zoom75)
        self.addAction (zoom75)

        zoom100= QAction (self)
        zoom100.setShortcuts ([QKeySequence.fromString ('F5')])
        zoom100.triggered.connect (self.zoom100)
        self.addAction (zoom100)

    def zoomfit (self, *ignore):
        winSize= self.size ()
        imgSize= self.img.size ()
        print winSize, imgSize
        hZoom= 1.0*winSize.width  ()/imgSize.width  ()
        vZoom= 1.0*winSize.height ()/imgSize.height ()
        zoomLevel= min (hZoom, vZoom)
        print zoomLevel
        self.zoomTo (zoomLevel)

    def zoom25 (self, *ignore):
        self.zoomTo (0.25)

    def zoom50 (self, *ignore):
        self.zoomTo (0.5)

    def zoom75 (self, *ignore):
        self.zoomTo (0.75)

    def zoom100 (self, *ignore):
        self.zoomTo (1.0)

    def zoomTo (self, zoomLevel):
        scale= zoomLevel/self.zoomLevel
        print "scaling", scale
        self.scale (scale, scale)
        self.zoomLevel= zoomLevel

if __name__=='__main__':
    # this code is enough for loading an image and show it!
    app= QApplication (sys.argv)
    win= QMainWindow ()

    pixmap= QPixmap (sys.argv[1])
    qgpi= QGraphicsPixmapItem (pixmap)
    scene= QGraphicsScene ()
    scene.addItem (qgpi)

    view= OMITGraphicsView (pixmap, scene, win)
    view.setDragMode (QGraphicsView.ScrollHandDrag)
    view.show()

    app.exec_ ()
    # up to here!

# end

Things to note:

  • The code for loading, showing the image and pan support is only 13 lines of Python code, including 3 imports. The resulting app is also able to handle vector graphics, but of course I didn't exploit that, I just added a QPixmap/QGraphicsPixmapItem pair.
  • Zooming is implemented via QGraphicsView.scale(), which is accumulative (scaling twice to 0.5 actually scales to 0.25 of the original size), so I have to keep the zoom level all the time. There should be a zoom() interface!
  • The code for calculating the scale level is not very good: scaling between 75% and 50% or 25% produces scales of 0.666 and 0.333, which I think at the end of the day will accumulate a lot of error.
  • For the same reason, zoomToFit() has to do some magic. I also got hit by the integer division of Python (I was getting zoom factors of 0) so I had to add 1.0* to the claculations. It's good that this is fixed in Python2.6/3.0.
  • The size reported by the QMainWindow was any vegetable (it said 640x480 when it actually was 960x600), so I used the QGraphicsView instead. WTF?
  • For some strange reason zoomToFit() scales the image a little bigger than it should, so a scrollbar appears in the direction of the constraining dimension.
  • Less that 100 lines! Even if setupActions() could surely be improved.
  • In Marcelo's favor I should mention that he writes docstrings for most of his methods both in english and spanish (yes, of course I read his code after I finished mine). I barely put a couple of comments, but doing the same should add 10 more lines, tops. Also, I don't want to convert this into a who-has-it-smaller contest (the code, I mean :).
  • It took me approx 3 hours, with no previous knowledge of how to do it and no internet connection, so no asking around. I just used the «Qt Reference Documentation», going to the «Gropued Classes» page and to the «Graphics View Classes» from there.
  • It doesn't zoom with the mouse wheel either.
  • The default colors of ikiwiki's format plugin are at most sucky, but better than nothing.

omit pykde python


[1] Unluckly he didn't declared which license it has, so I'm not sure if I really can do this. I GPL'ed mine.

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

I figured several things after the last/first release. One of those is that one can't try to pull a beta of your first releases. Betas are for well stablished pieces of code which are supposed to be rock solid; initial releases not. Another thing I figured out (or actually remembered) is that old saying: release early, release often.

So instead of a 0.1 'official' release, where all the bugs are nailed down in their coffins and everything is as peachy and rock solid as a peachy huge rock (like the Mount Fitz Roy[1], for instance), and only 13 days later than the initial release, we get another messy release: satyr-0.2, codenamed "I love when two branches come together", is out.

This time we got that pharaonic refactoring I mentioned in the last release, which means that skins are very independient from the rest of the app, which is good for skins developers and the core developers, even if those sets are equal and only contain me.

From the user point of view, the complex skin is nicer to see (column widths and headers, OMG!) and it also allows tag editing. Yes, because we have tag editing! Right now the only way to fire the edition is to start typing, which will erase previous data, but don't worry, I plan to nail that soon. At least it's usefull for filling up new tags. I also fixed the bug which prevented this skin to highlight which is being played. Lastly but not leastly, the complex skin has a seek bar, and the code got tons of cleanups.

So, that's it. It's out, go grab it!


satyr pykde python


[1] Right now I would consider satyr just a small peeble in a highway, only noticeable if some huge truck picks it up with its wheels and throws it to your windshield. But I plan to reach at least to be a sizable rock such as that one found near one of the Vikings in Mars.

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

Since a long ago I'm looking for a media player for listening my music. In that aspect I'm really exigent. Is not that I need lots of plugins and eye candy, no, I just need a media player that fits my way to listen music.

How do I listen to music? All day long, for starters. I have a collection of .ogg files, which I normally listen in random mode. From time to time I want to listen certain song, so I either queue it or simply stop the current one and start the one I want. Sometimes I enqueue several songs, which might not be related between them (maybe only in my head they are).

I've been using Amarok, I really like its random albums feature; that is, I listen to a whole album, and when it finishes, another album is picked at random and I listen to all its songs. The last feature, a really important one: My collection is my playlist and viceversa. I don't build playlists; if I want to listen to certain songs I just queue them. One feature I like also is a tag editor and the posibility to rearrange the songs based on its tags (with support for albums with songs from various authors, like OST's). Last but no least, reacting to new files in the collection is also well regarded.

I used to use xmms. I still think it's a very good player for me, but it lacks utf support and doesn't react when I add songs to the collection. Then I used Amarok, Juk, QuodLibet, Audacious (I was using it up to today) and probably a couple more. None of them support all the features, so today, completely tired of this situation, I started writing my own. I called it Satyr. Another reason to do it is to play a little more with PyKDE. Talking about Python and KDE, I know the existence of minirok, but it uses GStreamer, and I wanted to play with Phonon.

So, what's different in this media player? If you think about it, if you have a CD (vinyl, cassettes maybe?) collection in your home, what you have is exactly that: a collection of Albums. Most media players manage Songs, grouping them in Albums and by Author too. Notably Amarok used to manage Albums with several artists (what's called a 'Various artists' Album), but since Amarok 2 it doesn't do it anymore, nor the queuing works. So the basic idea is exactly that: you have a Collection of Albums, most of them with songs from the same Author (and sometimes Featuring some other Authors[1]), but sometimes with Songs from different Authors. Of course I will try to implement all the features I mentioned above.

Ok, enough introduction. This post was aimed to show some code, and that's what I'm going to do now. This afternoon I was testing the Phonon Python bindings, trying to make a script to play a simple file. This snippet works:

# qt/kde related
from <span class="createlink">PyKDE4</span>.kdecore import KCmdLineArgs, KAboutData, i18n, ki18n
from <span class="createlink">PyKDE4</span>.kdeui import KApplication
# from <span class="createlink">PyKDE4</span>.phonon import Phonon
from <span class="createlink">PyQt4</span>.phonon import Phonon
from <span class="createlink">PyQt4</span>.QtCore import SIGNAL

media= Phonon.MediaObject ()
ao= Phonon.AudioOutput (Phonon.MusicCategory, app)
print ao.outputDevice ().name ()
Phonon.createPath (media, ao)
media.setCurrentSource (Phonon.MediaSource ("/home/mdione/test.ogg")
media.play ()

app.connect (media, SIGNAL("finished ()"), app.quit)
app.exec_ ()

Of course, this must be preceded by the bureaucratic creation of a KApplication, but it basically plays an ogg file and quits. You just have to define a MediaObject as the source, an AudioOutput as the sink, and then you createPath between them. As you can see, with Phonon you don't even have to worry about where the output will be going: that is defined by the system/user configuration. You only have to declare that your AudioOutput is going to play Music (the second actual line of code).

There are a couple of peculiarities with the Python bindings. First of all, Phonon comes both with Qt and separately. The separate one has a binding in the PyKDE4 package, but it seems that it doesn't work very well, so I used the PyQt binding. For that, I had to install the python-pyqt4-phonon package. Second, the bindings don't support to call setCurrentSource() with a string; you have to wrap it in a MediaSource. The original API supports it. Third, it seems that Phonon.createPlayer() is not supported by the bindings either, so I had to build the AudioOutput by hand. I don't care, it's just a couple lines more.

This code also shows the name of the selected OutputDevice. I my machine it shows HDA Intel (STAC92xx Analog).

In the following days I'll be posting more info about what comes out of this project. I will only reveal that right now the code has classes called Player, PlayList and Collection. It can scan a Collection from a path given in the command line and play all the files found there. Soon more features will come.

python pykde satyr


[1] I'm not planing to do anything about it... yet.

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

satyr got the possibility to queue songs for playing very early. At that moment there wasn't any GUI, so the only way to (de)queue songs was via dbus[1]. Once the GUI was there, we had to provide a way to queue songs. As satyr aims to be fully usable only using the keyboard, the obvious way was to setup some shortcut for the action.

Qt and then KDE have a very nice API for defining 'actions' that can be fired by the user. The ways to fire them include a shortcut, a menu entry or a button in a toolbar. I decided to go with the KDE version, KAction.

According to [the documentation](http://api.kde.org/4.x-api/kdelibs-apidocs/kdeui/html/ classKAction.html#_details), to create an action is just a matter of creating the action and to add it to an actionCollection(). The problem is that nowhere it says where this collection comes from. There's the KActionCollection class, but creating one and adding the actions to it seems to be not enough.

If you instead follow the tutorial you'll see that it refers to a KXmlGuiWindow, which I revised when I was desperately looking for the misterious actionCollection(). I don't know why, but the documentation generated by PyKDE does not include that method. All in all, the tutorial code works, so I just ported my MainWindow to KXmlGuiWindow:

class MainWindow (KXmlGuiWindow):
    def __init__ (self, parent=None):
        KXmlGuiWindow.__init__ (self, parent)
        uipath= __file__[:__file__.rfind ('.')]+'.ui'
        UIMainWindow, _= uic.loadUiType (uipath)

        self.ui= UIMainWindow ()
        self.ui.setupUi (self)
        [...]
        self.ac= KActionCollection (self)
        actions.create (self, self.actionCollection ())

and actions.create() is:

def create (parent, ac):
    '''here be common actions for satyr skins'''
    queueAction= KAction (parent)
    queueAction.setShortcut (Qt.CTRL + Qt.Key_Q)
    ac.addAction ("queue", queueAction)
    # FIXME? if we don't do this then the skin will be able to choose whether it
    # wants the action or not. with this it will still be able to do it, but via
    # implementing empty slots
    queueAction.triggered.connect (parent.queue)

Very simple, really. But then the action doesn't work! I tried different approaches, but none worked.

The tutorial I mentioned talks about some capabilities of the KXmlGuiWindow; one of them, the ability to have the inclusion of actions in menues and toolbars dumped and loaded from XML file (hence, the XML part of the class name), and that is handled by the setupGUI() method. From its documentation: «Configures the current windows and its actions in the typical KDE fashion. [...] Typically this function replaces createGUI()». In my case the GUI is already built by that self.ui.setupUi() that you see up there, so I ignored this method. But the thing is that if you don't call this method, the actions will not be properly hooked, so they don't work; hence, my problem. But just calling it make the actions work! I'll check later what's the magic behind this method. For now just adding self.setupGUI() at the end of the __init__() was enough.

So, that's it with actions. As a result I also get several things: a populated menu bar with Settings and Help options (but no File; that'll come later with standard actions, of which I'll talk about later, I think), with free report bug, about satyr and configure shortcuts options, among others. The later does work but its state is not saved. That also will come in the same post that standard actions.

PD: First post from my new internet connection. Will satyr suffer from this? Only time will tell...


satyr pykde python


[1] then you had to pick the index of the songs either by guessing or looking at the dump of the playlist.

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

Tal vez ya lo leyeron en otro lado, pero bueno: el otro día fue el día de los tutoriales de Kubuntu. Básicamente fueron tutoriales por IRC. Los logs los pueden encontrar en el wiki de Kubuntu. En particular hay 3 que me parecen bastante piolas:

Como para salir masomenos andando están muy piolas.

bazaar pykde

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

I seem to have fixed the bug I mentioned in my last post. This is what I had:

class <span class="createlink">MetaPlayer</span> (type (QObject), type (dbus.service.Object)):
    """Dummy metaclass that allows us to inherit from both QObject and
    d.s.Object"""
    pass

class Player (QObject, dbus.service.Object):
    __metaclass__= <span class="createlink">MetaPlayer</span>
    [...]

Notice that MetaPlayer doesn't have a explicit __init__() method; one would spect that Python would take are of that. Here's the fixing code:

<span class="createlink">MetaQObject</span>= type (QObject)
<span class="createlink">MetaObject</span>= type (dbus.service.Object)

class <span class="createlink">MetaPlayer</span> (MetaQObject, <span class="createlink">MetaObject</span>):
    """Dummy metaclass that allows us to inherit from both QObject and d.s.Object"""
    def __init__(cls, name, bases, dct):
        <span class="createlink">MetaObject</span>.__init__ (cls, name, bases, dct)
        <span class="createlink">MetaQObject</span>.__init__ (cls, name, bases, dct)

I really don't understand why I have to be so explicit. Maybe it's because the metaclass for dbus.service.Object, dbus.service.InterfaceType, inherits from the type type[1]; this type is a new style class[2], but doesn't inherits from object. Thus, I think, the inherited __init__() methods are not called automatically.

In any case, now I can mix QObject and dbus.service.Object, and it works fine. For instance, this call works:

$ qdbus org.kde.satyr /player quit

dbus python pykde


[1] the type type is of type type! here:

In [1]: type (type)
Out[1]: <type 'type'>

[2] its type is not instance but type, as mentioned above.

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

In my last post I said «The next step is to make my Player class to export its methods via DBus and that's it!». Well, tell you what: is not that easy. If you try to inherit from QObject and dbus.service.Object you get this error:

In [3]: class Klass (QtCore.QObject, dbus.service.Object): pass
<span class="createlink">TypeError</span>: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a
    (non-strict) subclass of the metaclasses of all its bases

This occurs when both ancestors have their own metaclasses. Unluckily Python doesn't resolve it for you. The answer is to create a intermediate metaclass which inherits from both metaclasses (which we can obtain with type()) and make it the metaclass of our class. In code:

class <span class="createlink">MetaPlayer</span> (type (QObject), type (dbus.service.Object)):
    """Dummy metaclass that allows us to inherit from both QObject and
    d.s.Object"""
    pass

class Player (QObject, dbus.service.Object):
    __metaclass__= <span class="createlink">MetaPlayer</span>
    [...]

Is that it now? Can I go and do my code? Unfortunately no. See this:

qdbus org.kde.satyr
/
/player
Cannot introspect object /player at org.kde.satyr:
org.freedesktop.DBus.Python.KeyError (Traceback (most recent call last):
  File "/usr/lib/pymodules/python2.5/dbus/service.py", line 702, in _message_cb
    retval = candidate_method(self, *args, **keywords)
  File "/usr/lib/pymodules/python2.5/dbus/service.py", line 759, in Introspect
    interfaces = self._dbus_class_table[self.__class__.__module__ + '.' + self.__class__.__name__]
<span class="createlink">KeyError</span>: '__main__.Player'
)

This is the class dbus.service.Object complaining something else. It's getting late here and I'm tired, so I'll continue tomorrow.

dbus python pykde satyr

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde

I must be crazy or something. Inestead of spending my last friday in France for this year partying in some bar I satyed home and produced another satyr release. The ChangeLog? Here, have some:

  • (De)Queueing Songs with the keyboard, with visual feedback.
  • Remembers window size.
  • Current song highlighted not with selection but with real color changes.
  • We can select Songs. Right now useful only for queuing several songs at the same time.
  • Workaround a bug in PyQt4 with the SeekBar.
  • Show the filepaths as much as possible in the user's encoding.
  • Hitting F2 in a cell edits its contents.
  • Silightly cleaner interface: don't show so many 0's.
  • bug with perv/next: weren't wrapping around.

Go get it from the Download area! I promise to party double tomorrorw...


satyr pykde python

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: pykde