A few days ago someone said something[1] that reminded me about my audio player, which I had abandoned for more than a year already. The reason was mostly that the two Phonon backends, VLC and gstreamer, for some reason or other couldn't play the files I had without any gaps between songs.

To be honest, the first bug end up being me not properly encoding the filenames. If you first URL-encoded the filename and then built a Q/KURL with that, then it's all fine. It took me more than 12 months and a few rereads of the thread to realize it. Fixes apart, it seems that the bug still exists for other instances of gstreamer errors, so we're not out of the woods. In any case, I switched to the VLC backend and it seems that now is able to fire the aboutToFinnish() signal properly, so for the moment I'm using that.

All that is fine, but that's not what I wanted to talk about in this post. Given that this project largely precedes my interest on testing, it has no testing at all. Most of the project is straightforward enough to almost no need any, but there's a critic part that would not suffer at all if it had any, namely the Collections handling, including passing files from one to another and automatically updating new/removed Songs[2].

So after fixing the bug mentioned above I tried to figure out the current state of affaires regarding Collections, and boy, they're in bad shape. The code was locally modified, never commited, deactivating any notifications of filesystem changes (new or removed files), and other code I can't really understand the purpose of.

Because if this last detail is that I decided to start testing three classes: Collection, which handles a set a Songs with a common root directory; CollectionAggregator, which handles a set of collections and should coordinate moving a Song from one Collection to another; and CollectionIndexer, which scans from a Collection's root dir to find Songs.

All went fine while I tested the first class, Collection. There was a tricky part where I had to setup a QApplication in order to make signals work. The problems began when I started testing CollectionIndexer. Tests started blocking endlessly, signals stopped being either emited or firing the connected slots, life was bad.

I tried to search the available documentation and mailing lists for a hint about the problem, but besides a quite complex example that didn't seem to properly converge to anything useful, I was mostly on my own.

This morning I got my eureka moment: I noticed that if I executed each test class by itself, it worked, but both at the same time blocked and never finished. Then I remembered something said in QApplication's documentation:

For any GUI application using Qt, there is precisely one QApplication object, no matter whether the application has 0, 1, 2 or more windows at any given time.

That was it: I was creating the application, first in the setUp() method, then as a class attribute, but I had one test class per class to test, each in its own file. Somehow this last fact lead me to think that somehow they were executed in separate processes, which is not true. Luckily, even with this limitation, there's none on the amount of times you can exec_() and quit() the same instance, so that's what I did: I created only one instance and reused it everywhere. I was already doing that for each test method, but again, somehow having several files mislead me to think they were isolated from each other.

So now all my unit tests work without mysteriously blocking forever. Now I just hope I can keep riding the success wave and bring satyr into good shape. A new release wouldn't hurt.


[1] No matter how much I try, I can't get any vaguer.

[2] Ok, maybe the Player/Playlist combo wouldn't hurt to have UTs either.


satyr pykde python

Posted Mon 13 Oct 2014 02:22:53 PM CEST Tags: satyr

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: satyr

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: satyr

There's a problem with Phonon. If you try to play a file in a format that it doesn't support, instead of failing or simply sending the finished() signal it just does nothing. I'll have to look more deeply into this and figure out if it's a bug or feature or how to work-around it.

For now I just decided to use python-magic to decide if the file is playable or not. This module uses libmagic, the same library that the file utility uses. But I hit a problem with it. One of its ussages is to call magic.file() with the filename, but if the filename contains a non-ascii character you get something like this:

Traceback (most recent call last):
  File "satyr.py", line 173, in next
    self.play ()
  File "satyr.py", line 128, in play
    mimetype_enc= self.magic.file (self.filename)
<span class="createlink">UnicodeEncodeError</span>: 'ascii' codec can't encode character u'\xed' in position 37: ordinal not in range(128)

I found a workaround. There's another usage, which is to call magic.buffer() with a piece of data to recognize. So I just simply open() the file, read() 4KiB and pass the data to that function.

f= file (self.filename)
data= f.read (4096)
mimetype_enc= self.magic.buffer (data)

Now it works properly. I will file a bug report to the file package once I resort out my mail setup (I cannot send bugs with reportbug).

python satyr

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

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: satyr

Continuing with the development of Satyr, which doesn't have any GUI. I thought it would be faster to make a DBus interface that a good GUI, not to mention more interesting.

The code snippet of today is this:

class DBusServer (dbus.service.Object):
    def __init__ (self):
        bus= dbus.SessionBus ()
        bus_name= dbus.service.BusName ('org.kde.satyr.dbus.test', bus=bus)
        dbus.service.Object.__init__ (self, bus_name, "/server")

    @dbus.service.method('org.kde.satyr.dbus.test', in_signature='', out_signature='')
    def bing (self):
        print "bing!"

    @dbus.service.method('org.kde.satyr.dbus.test', in_signature='', out_signature='')
    def quit (self):
        app.quit ()

dbus.mainloop.qt.DBusQtMainLoop (set_as_default=True)
dbs= DBusServer ()

sys.exit (app.exec_ ())

This simply defines a class which registers itself with the session bus under the name org.kde.satyr.dbus.test, exporting itself under the path /server and then defining a method that goes bing! :) and another one that quits the app. Note the decorator for the methods.

You might notice the dbus.mainloop.qt.DBusQtMainLoop (set_as_default=True) call. This is needed because both Qt and DBus in asyncronous mode (which is the one we're using and the only one that works under Qt or Gtk, AFAIK) both have their own event loops, and that makes some kind of magic that let both loops coexist without blocking the other. This must be called before connecting to the bus; otherwise, you get this error:

<span class="createlink">RuntimeError</span>: To make asynchronous calls, receive signals or export objects, 
D-Bus connections must be attached to a main loop by passing mainloop=... 
to the constructor or calling dbus.set_default_main_loop(...)

So, let's test the beast. We run the script in one terminal and in the other:

mdione@mustang:~/src/projects/satyr/live$ qdbus | grep satyr
 org.kde.satyr.py-10154
 org.kde.satyr.dbus.test
mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test
/
/server
mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server
method void org.kde.satyr.dbus.test.bing()
method QString org.freedesktop.DBus.Introspectable.Introspect()
mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server org.kde.satyr.dbus.test.bing
mdione@mustang:~/src/projects/satyr/live$ qdbus org.kde.satyr.dbus.test /server org.kde.satyr.dbus.test.quit

And in the other console:

mdione@mustang:~/src/projects/satyr/live$ python dbus_test.py
bing!

It goes bing!... and then finishes. The next step is to make my Player class to export its methods via DBus and that's it!. More info in the Python DBus tutorial.

dbus python pykde satyr

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

For a couple of months I've been globbing about PyKDE4 stuff, and laterally talking about my last project: satyr. satyr (it's name should always be written in lowercase) should have the following features:

  • The PlayList and the Collection(s)[0] are the same thing.
  • Yours is a Collection of Albums, nothing else[3].
  • Some Albums are from the same artists and some are compilations[3].
  • If you want an ephemeral playlist you could queue songs[1].
  • If you want non-ephemeral playlists, then this player is not for you.
  • Ability to search à la xmms, but in the same interface[2]
  • Tag reading and writing[3].
  • Order you collection based on the tags[3].
  • The collection discovers new files and adds them to the playlist on the fly[4].
  • Be able to use all the program only with your keyboard (die, mouse, die!)

This and other features should be available soon. The coding has been fast lately, mainly because the Qt/KDE libs are fantastic to work with. The only thing I couldn't do was to read the tags before playing them, so I relied into the kaa libraries.

The project is hosted in savannah, and right now there is no tarball (It's marked as alpha state because I sent a couple of tarballs to some friends who asked for them), so the only way right now is to branch anonimously the bazaar repo. I hope you download and enjoy it as much as I do.

satyr pykde


[0] The support for several collections is not complete yet.

[1] Functionality available via dbus only at the moment.

[2] This is with the current GUI. I'm also thinking in several/pluginable GUI's

[3] Not yet available.

[4] Of course this only works if it's running. Otherwise, you can always ask for a rescanning[1].

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

So far both the default and complex skins in satyr were (ab)using the selection in their respectives views to highlight the current song and, in the case of the complex skin, that meant that you couldn't select cells. This could be (and will be) handy for editing several tags at the same time or for grouping songs in an Album. I decided that this should not longer be this way.

It was really simple: I simple reenabled selection, the extended version (Shift and Control work as you grew to spect). Now the way to highlight the current song is to simply change the background and foreground colors for the cells of that row. How? Simple, extending the QPlayListModel.data() method to provide these properties and using QApplication.palette() to get colors that would stand out from the normal application colors (even the selection) and would still make the text readable:

    def data (self, modelIndex, role):
        if modelIndex.isValid () and modelIndex.row ()<self.aggr.count:
            [...]
            # check the row so we highlight the whole row and not just the cell
            elif role==Qt.BackgroundRole and modelIndex.row ()==self.parent_.modelIndex.row ():
                # highlight the current song
                # must return a QBrush
                data= QVariant (QApplication.palette ().dark ())

            elif role==Qt.ForegroundRole and modelIndex.row ()==self.parent_.modelIndex.row ():
                # highlight the current song
                # must return a QBrush
                data= QVariant (QApplication.palette ().brightText ())
            [...]

And that's almost it... because there's always a but. Whenever the current song changes, there's a little lag between this happens (you can hear the new song being played) and the highlighting updates. This is because I update that self.parent_.modelIndex you see up there, but the QPLM.data() method is not called until the next repaint, which doesn't happen inmediately. So, we must force that repaint. How? Again, it's simple: we already emited the dataChanged() signal in the method setData() when the data could be successfully changed. Now we do the same just so the view updates the highlighting:

    def showSong (self, index):
        [...]
        # FIXME? yes, this could be moved to the model (too many self.appModel's)
        start= self.appModel.index (oldModelIndex.row (), 0)
        end=   self.appModel.index (oldModelIndex.row (), columns)
        self.appModel.dataChanged.emit (start, end)

        start= self.appModel.index (self.modelIndex.row (), 0)
        end=   self.appModel.index (self.modelIndex.row (), columns)
        self.appModel.dataChanged.emit (start, end)
        [...]

satyr pykde python

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

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: satyr

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: satyr