After several months thinking about it, and just two requests, I finally decided
satyr's code. I decided to use github because I already switched 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,
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
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
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!
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,
for instance), and only 13 days later than the initial release, we get another
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!
 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.
Update before even publishing: most of the numbers in the initial writing were
almost doubled. The problem was that
distutils left a
build directory when I
tried either to install or to package
satyr, I don't remember which, so the
files found by the
find commands below were mostly duplicated. I had to remove
the directory and run all the commands again!
I wanted to know some things about
satyr's code, in particular some statistics
about its lines of code. A first approach:
$ find . -name '*.py' -o -name '*.ui' | xargs wc -l | grep total 2397 total
Hmm, that's a lot, I don't remember wrinting so many lines. Beh, the comments, let's take them out:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^#' | wc -l 2136
What about empty lines?:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^(#.*| *)$' | wc -l 1764
Meeh, I didn't take out all the comment lines, only those lines starting
#, which are mainly the license lines on each source file. I have to also
count the comments in the middle of the code:
$ find . -name '*.py' -o -name '*.ui' | xargs egrep -v '^( *#.*| *)$' | wc -l 1475
And how much of those lines are actual code and not from some xml file describing user interface?:
$ find . -name '*.py' | xargs egrep -v '^( *#.*| *)$' | wc -l 1124
How much code means its 3 current skins?:
$ find satyr/skins/ -name '*.py' | xargs egrep -v '^( *#.*| *)$' | wc -l 341
How much in the most complex one?
$ egrep -v '^( *#.*| *)$' satyr/skins/complex.py | wc -l 182
All this numbers tell something: ~300 empty lines means that my code is not very tight. I already knew this: I like to break functions in secuential blocks of code, each one accomplishing a somehow atomic step towards the problem the function tries to solve. Almost 300 comment lines means my code is very well commented, even if a sixth of those comments are BUGs, TODOs, FIXMEs or HINTs:
$ find . -name '*.py' | xargs egrep '^ *# (TODO|BUG|FIXME|HINT)' | wc -l 56
Wow, more than I thought. Now, 1100 lines of actual code for all that
accomplishes today (playing almost any existing audio file format, progressive
collection scanning, lazy tag reading and also tag writing, 3 skins, handle
Collections, searching, random, stop after, saving some state, picking
up new songs added to the collection via the filesystem, queuing songs, dbus
interface, handle any encodings in filenames... phew! and some minor
features more!) I think is quite impressive.
Of course, doing all that in athousandsomething lines of code would be nearly
PyKDE4/KDE (something) 4,
Python itself. It's really nice to have such a nice framework to
work with, really.
 No user interface for this yet; shame on me.
 ... which toguether with
qdbus make my alarm clock.
 Almost all the support is in
  Less than 800 if we don't count skins.
 Yes, I add more footnotes as I readproof the posts :)
 I skipped  :)
 After the rebranding I don't know which is the proper name for the libraries, because I'm writing this post while very much offline, and TheDot does not publish the whole articles via rss, which I hate.
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
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
class to export its methods via
DBus and that's it!. More info in the
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
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
Satyr. Another reason to do it is to play a little more with
PyKDE. Talking about
KDE, I know the existence of
minirok, but it uses
GStreamer, and I wanted to play with
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), 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
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
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
The original API supports it. Third, it seems that
is not supported by the bindings either, so I had to build the
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
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,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.
 I'm not planing to do anything about it... yet.
So I mostly finished implementing a
QAbstractTableModel subclass with
read/write support, and some more stuff. Here's a resume of what changed from
the implementation of a read-only
columnCount(), the first important difference occurs
data() itself. Now the
QModelIndex.column() is important, because it
tells us which column the view wants to populate. In the case of the
DisplayRole I ask the
Song for the corresponding atribute/key; in the
case of the
SizeHintRole I use a static array of strings to provide default
values. I will explain why later:
def data (self, modelIndex, role): if modelIndex.isValid () and modelIndex.row ()<self.aggr.count: song= self.aggr.songForIndex (modelIndex.row ()) if role==Qt.DisplayRole: attr= self.attrNames [modelIndex.column ()] data= QVariant (song[attr]) elif role==Qt.SizeHintRole: size= self.fontMetrics.size (Qt.TextSingleLine, self.columnWidths[modelIndex.column ()]) data= QVariant (size) else: data= QVariant () else: data= QVariant () return data
The second difference is related to the view itself. A
QTableView presents not
only the contents but also what they call 'headers'. These are the column
headers ans row numbers that normally appear on top and left of tables. By
default numbers are shown, which in may case is ok for rows, but not for
columns. So we implement
headerData(), which is somehow similar to
section parameter refers to the column when direction is
def headerData (self, section, direction, role=Qt.DisplayRole): if direction==Qt.Horizontal and role==Qt.DisplayRole: data= QVariant (self.headers[section]) else: data= QAbstractTableModel.headerData (self, section, direction, role) return data
Once I have a basic read-only table, I start nitpicking on its appearance. First, all the columns have the same width, which is not acceptable. I want narrower 'Year', 'Track' and 'Length' columns, wider 'Artist', 'Album' and 'Title' columns and a huge 'Filepath' column. So, we set the column widths from the ui:
class MainWindow (KMainWindow): [...] def connectUi (self, player): [...] # FIXME: kinda hacky self.fontMetrics= QFontMetrics (KGlobalSettings.generalFont ()) for i, w in enumerate (self.model.columnWidths): self.ui.songsList.setColumnWidth (i, self.fontMetrics.width (w)) class QPlayListModel (QAbstractTableModel): def __init__ (self, aggr=None, songs=None, parent=None): QAbstractTableModel.__init__ (self, parent) [...] # FIXME: hackish self.columnWidths= ("M"*15, "M"*4, "M"*20, "M"*3, "M"*25, "M"*5, "M"*100)
Now the rows, the rows! They're too thick! I can only see tweintysomething songs
in my 1440x900 screen with a small plasma panel on top, and I already got rid of
the empty menubar and the status bar.
headerData() is also called with a
SizeHintRole, so let's use it (once more, the third time already, using the
QFontMetrics). Note that it also sets the width; and while I'm on it,
I also align the row numbers to the right:
def headerData (self, section, direction, role=Qt.DisplayRole): if direction==Qt.Horizontal and role==Qt.DisplayRole: data= QVariant (self.headers[section]) elif direction==Qt.Vertical: if role==Qt.SizeHintRole: # again, hacky. 5 for enough witdh for 5 digits size= self.fontMetrics.size (Qt.TextSingleLine, "X"*5) data= QVariant (size) elif role==Qt.TextAlignmentRole: data= QVariant (Qt.AlignRight|Qt.AlignVCenter) else: data= QAbstractTableModel.headerData (self, section, direction, role) else: data= QAbstractTableModel.headerData (self, section, direction, role) return data
Now I can see 36 rows maximized and almost 39 when in full screen. Maybe I can make the columns headers thinner... but once more I digress.
Now to the interesting part: making the model read/write. First we must
implement a method called
flags() which says a lot of things about cells; in
particular, if they're editable or not. Clearly 'Length' and 'Filepath' are not
def flags (self, modelIndex): ans= QAbstractTableModel.flags (self, modelIndex) if modelIndex.column ()<5: # length or filepath are not editable ans|= Qt.ItemIsEditable return ans
setData() itself. It has a similar interface as
including the new value. It should return if it could successfully change the
data value, which in this case means if
Tagpy/Taglib could modify the tag.
Note that the role while changing the value is
def setData (self, modelIndex, variant, role=Qt.EditRole): # not length or filepath and editing if modelIndex.column ()<5 and role==Qt.EditRole: song= self.aggr.songForIndex (modelIndex.row ()) attr= self.attrNames[modelIndex.column ()] try: song[attr]= unicode (variant.toString ()) except : ans= False else: self.dataChanged.emit (modelIndex, modelIndex) ans= True else: ans= QAbstractTableModel.setData (self, modelIndex, variant, role) return ans
It's really that simple. Now we have a
QTableView and a
which are read/write. This means that
satyr can modify and save tags now,
which is one of my most wanted features.
One last detail: for the way
satyr has its
QTableView configured, one can
enter in edit mode only by typing something in a cell, but then there are two
columns which cannot be modified. If you type something while the cursor is in
one of these columns,
QTableView will call
match() for matches to the typed
letters. The default implementation basically searches through all the cells,
which means that
data() is called for all the
Songs, which finally implies
that all the tags are read from disk, which is expensive for making the simple
mistake of typing in a read-only column. As
satyr already has a search
feature, I fixed this 'misfeature' with this code:
def match (self, start, role, value, hits=1, flags=None): # when you press a key on an uneditable cell, QTableView tries to search # calling this function for matching. we already have a way for searching # and it loads the metadata of all the songs anyways # so we disable it by constantly returning an empty list return 
 I have my reasons to provide both interfaces.
 Plans for saving the state of the
satyr main window will include the
column widths, but that's another story.
 It's usefull for writing tags of Songs with no tags... and because I can!
 ... which might return in the future to show scanning process, but again I digress.
 Yes, another digression: at some point it will be possible to move the file to a filepath based on its tags; in other words, to arrange your music collection based on tags. Should not be too far in the future.
 Last digression: I will modify this behaviour as soon as I finish with this post.
For a couple of months I've been globbing about
satyr (it's name should always be
written in lowercase) should have the following features:
- The and the Collection(s) are the same thing.
- Yours is a Collection of Albums, nothing else.
- Some Albums are from the same artists and some are compilations.
- If you want an ephemeral playlist you could queue songs.
- If you want non-ephemeral playlists, then this player is not for you.
- Ability to search à la
xmms, but in the same interface
- Tag reading and writing.
- Order you collection based on the tags.
- The collection discovers new files and adds them to the playlist on the fly.
- 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
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
repo. I hope you download and
enjoy it as much as I do.
 The support for several collections is not complete yet.
 Functionality available via dbus only at the moment.
 This is with the current GUI. I'm also thinking in several/pluginable GUI's
 Not yet available.
 Of course this only works if it's running. Otherwise, you can always ask for a rescanning.
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) 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 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.
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= mime.name () if mimeType=='application/octet-stream': mimeType= KIO.NetAccess.mimetype (url, self); else: # asumo que es html mimeType= "text/html" ptr= KTrader.self().query(mimeType, "'KParts/ReadOnlyPart' in <span class="createlink">ServiceTypes</span>") part= createReadOnlyPart (ptr.library (), tab, ptr.name ())
esto hace la fantástica magia de fijarse quées el link (dado por url) y luego KTrader me entrega un KPart que sabe mostrar ese . simplemente la embebo en un tab y ya. juzguen ustedes.
ok, suficiente por ahora. ya estaré hablando de éste y otros proyectos.
 no es una traducción literal del "whatnot" en inglés, sino una reimplementación en castellano de la misma idea.
 uso muchos términos en inglés que ni me gasto en traducir. deal with it.
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:
Cómo usar bazaar (con Launchpad, esa bonga de project server cerrado que usan los *buntu guys).
Empaquetando para Kubuntu (obvio, sirve para Debian también).
Tutorial de , el cual lamebtablemente es muy corto.
Como para salir masomenos andando están muy piolas.