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

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

#     * 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 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)

    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: tags/pykde

One of the things I had to while developing satyr is building a model for a QListViewer. It should be straighforward from qt's documentation, but I found a couple of things that I would like to put in a post, specially because there doesn't seem to be much models in PyQt4 easily found in the web.

According to its description, a subclass of QAbstractListModel as this one should mostly implement the data() and rowCount() methods, which is true. This example creates a read-only model, so no need to implement setData(), but given the simplicity of data(), it doesn't seem too difficult to do. I also wanted it to react when more Songs were added on the fly[1].

The method data() is the most important one. It is not only used for retrieving the data itself, but also some metadata useful for showing the data, like icons and other stuff. For selecting what the caller wants, it refers a Qt.ItemDataRole. The role for the data itself is Qt.DisplayRole. One of the particularities of this method is that it could be called with any vegetable as input; namely, it can refer to a row that does not exist anymore or for metadata that you don't care about. In those cases you must return an empty QVariant, not None. So, a first implementation is:

def data (self, modelIndex, role):
    if modelIndex.isValid () and modelIndex.row ()<self.count and role==Qt.DisplayRole:
        # songForIndex() returns the Song corresponding to the row
        song= self.songForIndex (modelIndex.row ())
        # formatSong() returns a QString with the data to show
        data= QVariant (self.formatSong (song))
        data= QVariant ()

    return data

This method, together with a rowCount() that simply returns self.count, is enough for showing data that is already there. Notice that the QModelIndex can be not valid, and in this case we only care about its row because we're a list.

But then I wanted my QListViewer to show songs progresively as they are loaded/scanned[2] and also as they are found as new. But then a problem arises: the view is like a table of only one column. The width of this colunm at the begining is the same width as the QListView itself. But what happens when the string shown is too big? What happens is that it gets chopped. We must inform the view that some of the rows are bigger. That's where the metadata comes into play.

Another possible role is Qt.SizeHintRole. If we return a size instead of an empty QVariant, that size will be used to expand the column as needed, even giving us a scrollbar if it's wider that the view.

Now, we're supposed to show the tags for the Song (that's what formatSong() does if possible; if not, it simply returns the filepath), so this width should be calculated based on the length of the string that represents the song[3]. But if we try to read the tags for all the songs as we load the Collection, we end up with too much disk activity before you can show anything to the user, which is unacceptable[4]. So instead we calculate based on the filepath, which is used for Songs with too few tags anyways. Here's the hacky code:

# FIXME: kinda hacky
self.fontMetrics= QFontMetrics (KGlobalSettings.generalFont ())
def data (self, modelIndex, role):
    if modelIndex.isValid () and modelIndex.row ()<self.count:
        song= self.songForIndex (modelIndex.row ())

        if role==Qt.DisplayRole:
            data= QVariant (self.formatSong (song))
        elif role==Qt.SizeHintRole:
            # calculate something based on the filepath
            data= QVariant (self.fontMetrics.size (Qt.TextSingleLine, song.filepath))
            data= QVariant ()
        data= QVariant ()

    return data

The last point then is reacting to Songs are added on the fly. This is also easy: you tell the views you're about to insert rows, you insert them, tell the views you finished, and then emit dataChanged():

def addSong (self):
    # lastIndex keeps track of the last index used.
    row= self.lastIndex
    self.lastIndex+= 1

    self.beginInsertRows (QModelIndex (), row, row)
    # actually the Song has already been added to the Collection[5]
    # so I don't do anything here,
    # but if you keep your rows in this model you should do something here
    self.endInsertRows ()

    self.count+= 1

    modelIndex= self.index (row, 0)
    self.dataChanged.emit (modelIndex, modelIndex)

Later I'll post any peculiarities I find porting all this stuff to a read/write QTableModel.

satyr pykde python

[1] That's material for another post :)

[2] This feature can be said to be a little too much. Actually, I get a flicker when scanning.

[3] Of course the next step is to use a table view and make a model for it.

[4] Right now the load time for a Collection of ~6.5k songs is quite long as it is.

[5] This is a design decision which is not relevant to this example.

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

kReiSSy (se pronuncia como la palabra "crazy" del inglés) es uno de mis proyectos más ambiciosos en este momento (tengo uno mayor, pero está en el freezer; ya hablaré de él). en resumen, puedo decir que kReiSSy es un lector de feeds (rss, atom, quéno[1]) alimentado a gofios, o como dicen ahora, en esteroides. otra descripción puede ser: es un concentrador de información externa con categorzación basada en tags.

¿qué features lo hacen tan pulenta? veamos:

  • tagging[2] de posts (eso lo hace cualquiera).
  • almacenamiento local de posts (en una base de datos, no borra ninguno).
  • browser integrado. acá empecé a irme al carajo.
  • tagging de páginas "leídas por ahí", que las convierte en first class citizens del programa.

¿qué significan estos dos últimos fatures? que puedo estar leyendo un post, seguir un link, de ahí a otro, y otro, todo en el browser integrado, y así hasta que encuentro otra página que nada que ver por dónde empecé, y entonces puedo tagear dicha página. esto hace dos cosas automáticas: guarda la página en la base de datos como si fuera un post más, y me permite luego buscar dicha página por tag.

ahora, me fui de boca un poco al decir "browser integrado" (esta es la sección "proyect status"). por ahora el browser nos permite ir hacia adelante, siguiendo links, pero no para atrás. es decir, le faltan todos los botones de navegación. también se podría marcar un post como "leer después", pero no hay código que lo haga. el filtrado por varios feeds o varios tags está roto/no anda (esto se debe o a un bug en SQLAlchemy o a mi inoperancia). aún no graba bien su sesión, cosa de volver en el mismo estado en que lo dejamos. y hace un par de chanchadas con los archivos, asume un par de paths y negradas así.

¿y en qué está hecha semejante bestia? python, obvio, mi lenguaje de cabecera desde hace unos añitos ya. pero no python puro, sino con varios agregados.

uno ya lo mencioné, SQLAlchemy, un ORM bastante potente, pues no fuerza muchas cosas. para usarlo como un ORM clásico (una clase por tabla) hay que hacer un par de giladas, pero nada grave. y permite hacer queries con SQL a lo macho, aunque no me llevo muy bien con eso. sqlite por debajo.

feedparser y beautifulsoup, un lector de múltiples tipos de feeds, y un html scrapper.

y la vedette de todos, y el motivo por el que empecé este proyecto: PyKDE. soy un usuario y fanático de kde desde que usé un redhat5.2 allá por el '98 o así. conozco bastante la infraestructura que hay por debajo, he leído varias veces cachos de código en busca de solucionar algún bug que me mordió, algunas veces hasta logré repararlo y todo. si bien su look no es muy bonito, la infraestructura que hay debajo es impresionante.

a tal punto que hacer este programa me resultó muy sencillo hasta ahora, pues sólo me concentré en mi funcionalidad. la parte de mostrado de html u otro tipo de archivos se lo dejé a KDE:

    mime= KMimeType.findByURL(url, 0, False, False)
    mimeType= ()
    if mimeType=='application/octet-stream':
        mimeType= KIO.NetAccess.mimetype (url, self);
    # asumo que es html
        mimeType= "text/html"

    ptr= KTrader.self().query(mimeType, "'KParts/ReadOnlyPart' in <span class="createlink">ServiceTypes</span>")[0]
    part= createReadOnlyPart (ptr.library (), tab, ())

esto hace la fantástica magia de fijarse qué MimeType es el link (dado por url) y luego KTrader me entrega un KPart que sabe mostrar ese MimeType. simplemente la embebo en un tab y ya. juzguen ustedes.

ok, suficiente por ahora. ya estaré hablando de éste y otros proyectos.

[1] no es una traducción literal del "whatnot" en inglés, sino una reimplementación en castellano de la misma idea.

[2] uso muchos términos en inglés que ni me gasto en traducir. deal with it.

kreissy python pykde

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

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 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 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: 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: tags/pykde

More than two months ago I globed about QStrings and paths. The problem was this: my app accepts paths via command line, which are processed via KCmdLineOptions; which in turn converts everything to QStrings. What I wanted were paths, which are more like QByteArrays, not QStrings (because the latter have internally an unicode representation; more on that later). Including PyQt4 in the equation forced me to resort to QByteArray to get the path as a str instead of using QString.constData() (PyQt4 doesn't export that function). But that's only the beginning of the problem.

Take for instance this situation. I have a music collection that I've been building for years now (more that 10, I think). In the old times of this collection the filenames were encoded in iso-8859-1. Then the future came and converted all my machines to utf-8. But only the software; the filesystems were in one way or another inherited from system to system, from machine to machine. So I ended with a mixture of utf and iso filenames, to the point where I have a file whose filename is in iso, but the directory where it is is in utf. Yes, I know, it is a mess. But if I take any decent media player, I can play the file allright. That's because the filesystem knows nothing of encodings (otherwise it would reject badly encoded filenames).

I just spent last saturday making sure that satyr only stored filepaths in strs, not unicodes or QStrings. It took concentration, but having just a bunch of classes and only 3 or 4 points where the filepaths are managed it wasn't that difficult. Still, it took a day. But then, as I mentioned in that post, Phonon the is not able to play such files... or so I thought.

If you run satyr after executing export PHONON_XINE_DEBUG=1 you'll see a lot of Phonon debug info in the console (not that there is another way to run satyr right now anyways). Among all that info you'll see lines such as these two:

void Phonon::Xine::XineStream::setMrl(const QByteArray&, Phonon::Xine::XineStream::StateForNewMrl) ...
bool Phonon::Xine::XineStream::xineOpen(Phonon::State) xine_open succeeded for m_mrl = ...

If you're sharp enough (I'm not; sandsmark from #phonon had to tell me) you'll note the mention of MRL's. MRL's are xine's URL for media. As any URL, they can (and most of the time must) encode 'strange' characters with the so-called "percent encoding". This means that no matter what encodings the different parts of a filepath is in, I just add file:// at the beginning and then I can safely encode it scaping non-ascii characters to %xx representations... or that's what the theory says. One thing to note is that the file:// part must not be scaped; xine complains that the file does not exist in that case.

Looking for help in Qt's classes one can find QUrl and the already known QByteArray. I can call QByteArray.toPercentEnconding() from my str and feed that to QUrl.fromPercentEncoding() (which strangely returns a QString, which is exactly what we're avoiding) or QUrl.fromEncoded(). But then the first function encodes too much, replacing :// with %3A%2F%2F. No fun.

Ok, let's try creating a QByteArray with only the file:// and then append() the toPercentEncoding() of the path only. It works:

<span class="createlink">PyQt4</span>.QtCore.QByteArray('file://%2Fhome%2Fmdione...%2F%C3%9Altimo%20bondi%20a%20Finisterre%2F07-%20La%20peque%F1a%20novia%20del%20carioca.wav')

But then calling QUrl.fromEncoded() gives:

<span class="createlink">PyQt4</span>.QtCore.QUrl("file://xn--/home/mdione.../ltimo bondi a finisterre/07- la pequea novia del carioca-wkmz60758d.wav")

The URL got somehow puny-encoded, which of course xine doesn't recognize for local files.

Another option is to create an empty QUrl, call setEncodedUrl() with the ParsingMode to QUrl.StrictMode so we avoid 50 lines of code that start here[1] that try to escape everything all over again (and I already had some double-or-even-triple-enconding nightmares parsing RSS/Atom feeds last year, thank you), but we get puny-encoded again (maybe it is 'pwny-encoded'?).

Last resort: backtrack to the point were we created only one QByteArray with the path and call toPercentEncoding(); feed that to the method setEncodedPath() of an empty QUrl. Then we add the last piece calling setScheme('file') and we're ready! Of course we're not:

<span class="createlink">PyQt4</span>.QtCore.QByteArray('file:%2Fhome%2Fmdione...%2F%C3%9Altimo%20bondi%20a%20Finisterre%2F07-%20La%20peque%F1a%20novia%20del%20carioca.wav')

Notice the lack of the two // after file:? xine doesn't like it; hence, I don't either.

Ok, this post got too long. I hope I can resolve this soon, I already spent too much time on it. At least a good part of it was expaining it, so others don't have to suffer the same as I did.

BTW, satyr will shortly be released, whether I fix this bug or not.

satyr pykde phonon

[1] Look at the size of that file! 6k lines to handle URL's! Who would say it was so difficult... Once more I'm remembered of how lucky I am to have this libraries at the tips of my fingers, yay!

Posted Wed 27 Jan 2010 11:55:55 PM CET Tags: 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") ()

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: 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, 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)
        [...] 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: tags/pykde