Minirok blog
(Para el II Concurso Universitario de Software Libre)
Un ganador que siempre llega tarde

Sigh; siempre tardo en bloguear las noticias importantes. En fin:

Enhorabuena a todos los ganadores y finalistas. También dar las gracias a la organización por lo bien que estuvo la fase final. He de confesar que me lo pasé muy bien, y que fue un gusto conocer a todos los que estuvieron allí.

Oh, y para demostrar que sigo con el proyecto, hoy he liberado Minirok 0.9.1.

21-05-2008
Omg, finalista y a Sevilla!

Oh, so no he podido pasarme por aquí a decirlo, pero al final Minirok es proyecto finalista del concurso! No puedo decir lo contento que estoy... Enhorabuena al resto de finalistas, y también a todos los participantes, porque creo que crear o participar en un proyecto de software libre es ya una recompensa en sí (yo estoy contentísimo de haber entrado en el mundillo hace ya 4 años o así).

Estas dos últimas semanas he estado bastante ocupado: una vez finalizada la fase de desarrollo volví corriendo a mi gran otra pasión que es Debian, y he estado involucrado en migrar de Python 2.4 a Python 2.5 en Lenny. Yay!

Y más. Ahora en un par de horas, cojo un tren y me voy a Coruña, a una reunión de los desarrolladores de Debian españoles (aunque vienen un par de fuera también), la segunda edición de la Dudesconf. Daré una charla introductoria sobre Git, y un par más de cosas relacionadas con Debian, y (sobre todo) pasaré unos días con un montón de amigos, porque eso son.

Nos vemos la semana que viene en Sevilla!

01-05-2008
Omg, DBus!!

En alguna entrada anterior comenté un poco de pasada los problemas que estaba teniendo con DBus. Qt4 proporciona un módulo QtDBus, que las aplicaciones de KDE4 utilizan para exportar interfaces. Sin embargo, este módulo no está disponible en PyQt, ya que a juicio del autor era demasiado difícil de wrappear, y tendría demasiadas limitaciones. Su recomendación es utilizar instead los bindings de Python para DBus, dbus-python.

El problema es que al utilizar dbus-python en Minirok, la aplicación se congelaba, debido (se ve) a una mala interacción entre estos bindings y QtDbus.

Jonathan Riddell me comentó que, según Trolltech, era posible que este problema se arreglara en Qt 4.4-rc1. Esta versión ha sido liberada hace poco, y ya está en la rama experimental de Debian. Me bajé los paquetes, lo probé y... voilà, funciona! Qué alegrón.

Así que ayer por la tarde la dediqué a portar la interfaz DCOP de Minirok a DBus. He subido el resultado al repositorio Git. Es una lástima que no haya dado tiempo a hacerlo en tiempo de concurso, pero bueno, mejor tarde que no nunca. ;) El diff es éste.

09-04-2008
Diálogo de preferencias, nueva página web, y repositorio Git

Bueno, pues la fase de desarrollo del concurso concluye. Esta última semana le he dado los últimos retoques al nuevo diálogo de preferencias, y lo he subido a una rama aparte (ya que, como comenté, hay un problema en PyKDE que hace que no funcione correctamente, y el autor anuncia que va a tardar un tiempo en arreglarse).

Por otra parte, he mejorado un poco la página web de Minirok, haciéndola algo más agradable estéticamente, y añadiendo un RSS de noticias (con, por ejemplo, las últimas versiones).

Por último, he decidido migrar el sistema de control de versiones de Minirok de Bazaar a Git. Esto no afecta al concurso, ya que todo el trabajo sigue estando en el repositorio Subversion de la Forja (y, de momento, tampoco he borrado el repositorio Bazaar, que sigue en el mismo sitio). El repositorio Git, aquí.

Y ya para terminar, me he dado cuenta que no he blogueado ninguna captura de pantalla de Minirok. En la página web hay una sección de screenshots, pero voy a poner aquí una al menos:

Un saludo, y mucha suerte a todos!!

06-04-2008
Filtrado de modelos Qt en Minirok

Introducción

La búsquedas en Minirok, tanto en la vista en árbol como en la lista de reproducción, se realizan limitando las filas visibles a aquéllas que contengan la cadena introducida en la caja de búsqueda. Es decir, se produce un filtrado. Para esto, Qt proporciona la clase QSortFilterProxyModel.

La idea de esta clase, como su nombre indica, es convertirse en un proxy entre el modelo fuente y la vista, de manera que la vista sólo se comunica con el proxy, el cual recibe peticiones de la vista y las remite al modelo fuente tras realizar las modificaciones pertinentes.

Por ejemplo, si el modelo fuente tiene 10 filas, pero hay un filtro en efecto que hace que sólo deben ser visibles la 4, la 5 y la 8, el proxy le dirá a la vista que hay 3 filas (y no 10), y cuando la vista solicite el texto de la fila 1 para mostrarlo, el proxy le devolverá el texto de la fila 4 en el modelo fuente (y lo mismo para las filas 2/5, y 3/8).

Cada vez que hay un cambio en el patrón del proxy, éste notifica a la vista de que deberá solicitar de nuevo las filas, pues éstas han cambiado (puede que ahora haya 4 filas visibles, por ejemplo, o 2).

Patrones multi-palabra

En Minirok, el comportamiento de las búsquedas consiste en mostrar las filas que contengan todas las palabras de la caja de búsqueda, en cualquier orden (por ejemplo, la búsqueda “como ana” debe incluir el fichero 09_Ana Torroja - Como sueñan las sirenas.mp3).

La clase QSortFilterProxyModel realiza el filtrado mediante expresiones regulares, las cuales son insuficientes (excepto mediante contorsiones muy desagradables) para realizar matching de todas las palabras sin importar el orden.

La solución a esto es proporcionar una subclase de QSortFilterProxyModel que proporcione una implementación alternativa del método virtual filterAcceptsRow() que sea capaz de buscar todas las palabras en cualquier orden. Para los curiosos, es la clase Model del fichero proxy.py.

Mapeado de métodos

Como he mencionado arriba, la vista se comunica única y exclusivamente con el proxy, y no con el modelo fuente. QSortFilterProxyModel proporciona reimplementaciones de, por ejemplo, data(); pero ¿qué pasa con otros métodos del modelo fuente?

La respuesta es que el modelo proxy ha de proporcionar wrappers de los métodos no estándar del modelo fuente. Si, por ejemplo, el modelo fuente tiene un método toggle_enqueued(index), el proxy deberá proporcionar un método con la misma signatura, que simplemente llame al método del modelo fuente previa traducción del índice del proxy al índice fuente mediante el método mapToSource() (éste es el método que convertiría la fila 1 a la fila 4 en el ejemplo de arriba).

Esto puede ser un poco tedioso de escribir y Python, al ser un lenguaje dinámico, tiene varios mecanismos para hacerlo más fácil. Uno de ellos es utilizar decorators, así:

  @map
  def toggle_enqueued(self, index):
      pass

Donde map es un decorador tal que:

  def map(method):
      """Decorator to invoke a method in sourceModel(), mapping one index."""
      def wrapper(self, index):
          index = self.mapToSource(index)
          return getattr(self.sourceModel(), method.func_name)(index)
      return wrapper

Cabe añadir que para que estos wrappers funcionen, los métodos adicionales en el modelo fuente deben tomar índices y no números de fila, tal y como era el caso en la versión original de mi clase Playlist. Antes de poder implantar el modelo proxy, tuve que cambiar varios métodos de tomar filas a tomar índices en la revisión 620. Así mismo, eliminé varios métodos query en favor de roles de usuario en data().

Problemas al filtrar un árbol

Una vez tuve el proxy para la lista de reproducción funcionando, me puse con la vista en árbol. Aquí, sin embargo, me encontré de inmediato con un problema: el caso en el que un fichero contiene todas las palabras de la búsqueda, pero los directorios padre no. En ese caso, el comportamiento de QSortFilterProxyModel es no mostrar a los padres ni, por tanto, al hijo que contenía las palabras!

La clase KListViewSearchLine de KDE3 hacía esto bien: con la función setKeepParentsVisible(), era posible mostrar los padres aunque no contuvieran las palabras, si algún hijo de ellos las contenía. No hay nada similar en Qt 4, pero escribí a Trolltech comentando el problema y lo han aceptado como sugerencia.

Mientras tanto, no obstante, hay que buscarse la vida. Básicamente lo que he hecho es delegar el filtrado al propio modelo fuente, de manera que cuando se le pregunta si un determinado índice es visible o no, primero visita (mediante llamadas recursivas) todos sus hijos para ver si son visibles (cacheando el resultado), y si alguno de ellos lo es, marca el índice original como visible. Cuando se le pregunte si algún hijo es visible, él cálculo ya estará realizado y devolverá el valor almacenado.

Ordenación

Por último, QSortFilterProxyModel también es capaz de proporcionar a la vista una visión ordenada del modelo original. Sin embargo, no he hecho uso de esa capacidad, ya que me era necesario tener los elementos ya ordenados en el modelo fuente, para que por ejemplo al usar drag & drop, la lista de ficheros generada esté en el orden correcto.

01-04-2008
Una historia de grrr, ooh y... yay?

Bueno. Me siento como si hubieran pasado meses desde la última vez que escribí aquí. Supongo que cuando uno está trabajando y no hace más que encontrarse con muros, el tiempo pasa más despacio. Esta entrada también se podría haber titulado: De cómo reescribí la vista en árbol en una semana. Ah, y perdón por adelantado, que esto va a ser largo.

En una entrada anterior comenté cómo los problemas de rendimiento al hacer búsquedas en la vista en árbol eran debidos a la propia librería Qt, y cómo sus autores me habían recomendado que la migrara al paradigma modelo/vista de Qt4. El mismo que la lista de reproducción ya usa.

Parte 1: Grrr, y más grrr

Comencé intentando utilizar algo ya existente en las librerías de KDE, y KDirModel parecía muy prometedor. Le dediqué un rato, y en un par de horas, puede que algo menos, tuve algo funcionando. Sin embargo, pese a ser fácil de utilizar, tuve que abandonarlo: era demasiado inflexible a la hora de realizar un listado recursivo del sistema de ficheros — y, de todas maneras, al no tener control de las estructuras internas, hubiera sido algo más complejo realizar búsquedas en dicha jerarquía.

A continuación, pues, comencé a implementar mi propio modelo, intentando preservar lo máximo posible del código existente para leer los directorios. Decidí representar el árbol como una lista de arrays y un mapa de índices, y me estuve peleando sin conseguir que funcionara: no conseguía encontrar el error por ninguna parte. Así las cosas, decidí desechar este modelo, y escribir uno nuevo basándome al pie de la letra en un ejemplo de la documentación de Qt.

Una vez tuve el modelo basado en el ejemplo casi funcionando, lo comparé al mío original y descubrí el error: había estado heredando de QAbstractTableModel en lugar de QAbstractItemModel! De todas maneras, me gustó más esta segunda manera (con un verdadero árbol de ítems, en lugar de una lista de arrays), y me lo quedé. Pero, terminé de copiar el ejemplo y... bum!, crash al expandir los ítems. Tras otro buen rato de depuración, encontré el fallo: pequeño error de transcripción del ejemplo (C++) a mi código (Python). Y mientras tanto el tiempo iba pasando...

Ya que estaba restructurando la vista en árbol, me dije que sería una buena oportunidad para implementar al mismo tiempo la tarea 790: leer el contenido de los directorios en un hilo aparte, para no bloquear la interfaz, particularmente si el directorio está en un sistema de ficheros en red. Pero na-nay: se seguía bloqueando la interfaz, y casi que más. Entonces pensé: con KDirModel no se bloqueaba, voy a ver si puedo investigar cómo lo hace.

Parte 2: Ooh, un KListDir

KDirModel utiliza kioslaves para realizar la lectura de los directorios. La ventaja principal de los kioslaves es que son un proceso separado, por lo que la aplicación no se bloquea en absoluto. Win!

En particular, KDirModel utiliza un objeto de la clase KDirLister, que es como una capa de abstracción sobre los kioslave. Y es una pa-sa-da: no sólo proporciona a la aplicación con la lista de contenidos de un directorio (filtrando incluso por tipo MIME, lo que le viene de perlas a Minirok), sino que se queda observando los directorios abiertos y notifica a la aplicación si se crean, borran o renombran ficheros en ellos!

El funcionamiento es sencillo, mediante señales de Qt. La aplicación solicita al objeto la lectura de un directorio con openUrl(), y el objeto devolverá los contenidos del mismo en una o varias señales newItems(), más una señal completed() al finalizar.

Así, la parte principal del modelo la componen los slots conectados a esas señales. Y la tarea 790, hecha!

Parte 3: Yay?

Tras tener una primera version del modelo con KDirLister hace unos días, no sé por qué no terminaba de quedarme un buen sabor de boca del todo. Me parecía que KDirLister iba un poco más lento que mi código anterior, y que utiliza más memoria. Sin embargo, al ir pasando los días este sentimiento se ha ido disipando, afortunadamente.

Porque ventajas las hay, por un tubo. No sólo el hecho de refrescar en tiempo real es fantástico, sino que ahora Minirok puede leer cualquier directorio al que KDE pueda acceder, por ejempo unidades compartidas de Windows. (Lamentablemente aún no podría reproducir de ellas, ya que el motor de reproducción es todavía GStreamer. Phonon no tiene Python bindings aún.)

Así que, me digo, creo que ha sido un cambio para mejor. Por cierto, el diff (gigante) es éste, y el mensaje de la revisión (la 631), aquí.

Addendum

“¿Y de la búsqueda qué?” Que, claro, fue el motivo inicial para todo esto. Pues, funciona razonablemente bien, aunque no ha sido un camino de rosas tampoco. Lo explicaré con más detalles en una entrada dentro de unos días.

Para terminar, dos cosas más:

29-03-2008
Minirok 0.9 liberado!

Lo tenía pendiente de hacer: acabo de liberar Minirok 0.9, posiblemente la última versión de Minirok para KDE 3. Incluye la última funcionalidad que he implementado para esta versión: búsqueda dentro de una pista mediante el slider de la barra de estado. A partir de ahora, ya no actualizaré esta rama (excepto bugfixes importantes), y todas las nuevas funcionalidades irán a la rama KDE 4.

He anunciado la liberación, como simpre, en Freshmeat, KDE-Apps.org, y he subido los paquetes y el tarball a la forja.

22-03-2008
Respuesta de Trolltech

No sé si lo comenté por aquí ya, pero en la nueva versión de Minirok pasa lo siguiente: la funcion de búsqueda en el árbol de ficheros es lenta, muy lenta. En la versión anterior funcionaba muy rápido incluso en mi colección de 70 GB de música, y en ésta tarda unos 10 segundos en devolver los resultados.

Investigando, averigüé que no era culpa de mi código, sino de la propia librería Qt. Había varios bugs abiertos al respecto en su sistema de informe de errores, y en algunos se decía que se había arreglado para la próxima version, la 4.4.0.

Así que me bajé la primera beta, pero el problema seguía allí. Les escribí comentándoselo, y ésta fue su respuesta:

In the latest snapshot for Qt 4 there is an improvement over Qt 4.3 with respect to the hiding of items in QTreeWidget, but what you really should be using is QTreeView. QTreeWidget sacrifices performance for the sake of convience and to have more control you should use QTreeView. Then using that in conjunction with QSortFilterProxyModel to actually filter out the ones you don’t want to see as opposed to directly hiding them should do the trick for you. If you run into any problems with this then please let me know.

Conclusión: que tengo que rescribir la vista en árbol si quiero un rendimiento aceptable. :-( Veré si me da tiempo antes de que termine el concurso.

20-03-2008
El resto de la semana

El resto de la semana ha sido un poco frustrante, la verdad. Primero estuve portando la interfaz DCOP a DBus, y me quedé atascado: se me queda colgado el programa, y el autor de PyKDE tampoco sabe qué hacer. Así que me puse con otra cosa: portar el diálogo de preferencias.

Como no era un diálogo muy grande y la conversión automática proporcionada por uic3 no me satisfizo mucho, lo volví a hacer con la versión 4 de Designer. Sin embargo, al migrar la clase heredada de KConfigSkeleton no parecía funcionar: los settings siempre me devolvían False. Tengo que investigar mañana, hoy ya no doy para más.

Una semana casi perdida, grrr.

16-03-2008
Empaquetando PyKDE para Debian (y Ubuntu), segunda parte

En una entrada anterior comenté de pasada que había estado preparando paquetes de PyKDE4 para Debian. Sin embargo, al final nunca subí aquella version a los servidores, por la siguiente razón.

Esa versión se basaba en el código de PyKDE presente en el repositorio Subversion de KDE mismo, y estaba preparada por Simon Edwards. Dicha versión, como también he comentado en otras entradas, tenía unos cuantos bugs, algunos de ellos importantes (como por ejemplo crashes al usar KConfig y clases vecinas).

Por otra parte, el autor original de PyKDE, Jim Bublitz, mencionó en la lista de correo que estaba preparando una versión propia para KDE 4 que liberaría en un mes o así y que arreglaría estos bugs. Jim hace sus liberaciones desde la página de Riverbank (autores de PyQt), y Simon Edwards dijo que, eventualmente, actualizaría su versión del repositorio de KDE a esta nueva versión de Jim.

Así, decidí no preparar paquetes desde la versión de Simon, y esperar a las versiones de Jim. Hace unos pocos días Jim liberó su versión, y yo comencé a empaquetarla. Es, la verdad, rehacer todo el trabajo hecho anteriormente, ya que esta versión utiliza un sistema de compilado completamente distinto (y algo menos idóneo para la construcción de paquetes), pero bueno. También, entre yo y otros, hemos ayudado a Jim descubrir algunos bugs en dicho sistema. Con esta tarea he estado ayer y hoy enteros, y con un poco de prisa...

Prisa, porque ya que iba a hacer el trabajo para Debian, me parecía muy importante que la próxima versión de Ubuntu (hardy) tuviera la versión de Jim (ya que si un usuario de Ubuntu prueba Minirok, y el programa aborta debido a un bug en PyKDE, no es muy agradable). Pero esa nueva versión de Ubuntu está ya muy próxima, de hecho la semana que viene sacan la primera beta y, como se anuncia aquí, mañana mismo empieza la congelación condujente a esa beta. Si quería ver mis paquetes en Ubuntu, había que darse prisa!

Tanto trabajo al fin se ha visto recompensado: por fin Jonathan Riddell ha subido mis paquetes a Ubuntu esta noche. Yay! (El repositorio con el empaquetado, aquí.)

12-03-2008
La semana del slider y de git

Bueno. Pues el lunes comencé con el slider, el punto #3 de la entrada anterior (el punto #1 lo hice durante el fin de semana, y el #2 prefiero guardármelo para más tarde).

Todo iba bien, no parecía complicado en exceso. En el lado del motor de reproducción (basado en GStreamer), una llamada a gst_element_seek_simple() en su método set_position(). En el lado de la interfaz, hay que conectarse a las señales sliderPressed(), sliderMoved() y sliderReleased() del slider. Cuando llega esta última, hay que invocar al método set_position() del motor; en sliderMoved() hay que actualizar el texto de las etiquetas en la barra de estado (para que haya indicación visual de la posición a la que se está cambiando).

Pero, no podía ser de otra manera, surgió un problema: gst_element_seek_simple() es asíncrono, es decir, retorna inmediatamente, antes incluso de que la búsqueda se haya realizado. Esto produce que, una vez el usuario suelta el slider, éste vuelva durante un instante a su posición original, para seguir con la nueva a continuación. La solución: conectarse al bus GStreamer para recibir notificación de cuándo se ha realizado de verdad la búsqueda, y no permitir al slider actualizar su posición hasta entonces. Es más código, claro, pero se hace y punto. ;-)

Pero, de nuevo, problemas: si hacía esto, el QTimerWithPause que utilizaba para actualizar segundo a segundo la barra de estado (y que pausaba hasta que se realizaba la búsqueda) parecía quedarse congelado tras reiniciarlo, o sus señales no llegaban a la barra, o qué sé yo. Estuve todo el lunes peleándome con esto, y no conseguí llegar a ningún sitio. Y me bloqueé. Y cuando me bloqueo no soy capaz de ser creativo y buscar otras soluciones, así que me puse a hacer otras cosas (ver abajo). Ayer, por fin, me sentí desbloqueado, y con fuerzas para atacar de nuevo el problema. Y encontré una solución (conectar y desconectar la señal del timer, en lugar de pausarlo). Tarea 559 terminada. \o/


El martes encontré otras tareas con las que desbloquearme. Por un artículo que leí, me decidí a probar Git, el sistema de control de versiones creado por Linus Torvalds. Explico mis experiencias en esta entrada de mi blog (en inglés).

Para seguir probándolo un poco más extensivamente, quise convertir algunos de mis repositorios privados de Bazaar a Git. Para ello, la manera más rápida es usar git-fast-import y un frontend para Bazaar. Como dicho frontend no existía, lo escribí.

¿Mi impresión sobre git? Lo que dicen en este comentario:

Aprender git es como aprender vi: supone un paradigma radicalmente diferente del de cualquier otro sistema de control de versiones o editor que puedas conocer. Pero una vez que superas ese problema, no querrás volver a los anteriores. Nunca.

Y uno es usuario de vim desde hace 7 años...


Ahora, a portar la barra de estado a KDE4!

08-03-2008
Playlist (y 6): library bugs, merging, the future, oh my!

Termino ya con esta entrada la serie. Espero no haber aburrido demasiado al personal. :-)

Library bugs

El migrado de la playlist a Qt4 ha puesto de manifiesto algunos bugs en esta versión de la librería. En particular, estos dos, de los que informé, mayormente cosméticos:

También me he topado con una multitud de bugs existentes sobre el rendimiento de ocultar filas en las vistas de ítems (por ejemplo, el #166175). Esto afecta directamente a la vista en árbol del sistema de ficheros, que puede ser muy grande y requerir ocultar muchas filas. Ahí se dice que está arreglado, pero en las pruebas que hice con la Technical Preview 1 de Qt 4.4 no me lo pareció — tendré que investigar.

Pasando ya de Qt a KDE, me encontré con un único bug, concerniente al sistema de ventanas y que un desarrollador arregló en menos de media hora! Así da gusto...

Por último, he podido encontrar un workaround para uno de los problemas que tenía con los bindings a Python de KDE. La solución, aquí.


Merging

Hoy he integrado la rama en la que realizé los cambios a la lista de reproducción (kde4_playlist) contra la rama principal del migrado a KDE4 (kde4). Bazaar hace de esto una tarea realmente sencilla, y como había realizado la integración inversa varias veces durante este mes (es decir, integrar kde4 contra kde4_playlist), no ha habido conflictos. La revisión es la 576 (full log, full diff).

Ha sido un mes entero de trabajo, con 142 revisiones en total (más algunas adicionales en otras ramas).


The future

Estos van a ser los siguientes pasos que voy a realizar para el proyecto:

  1. hacer funcionar el enviado de pistas a Last.fm; el código en sí necesitará pocos cambios, pero tendré que portar la clase QTimerWithPause primero.

    En cuanto haga esto, yo creo que podré usar la versión KDE4 de Minirok como mi reproductor principal. \o/

  2. migrar el diálogo de preferencias. Esto requiere tanto migrar la parte gráfica (como se explica aquí), como utilizar el nuevo framework de configuración de KDE4.

  3. llegados a este punto, volveré a la versión estable de Minirok, e implementaré el cambio de posición dentro de una pista mediante el slider de la barra de estado (tarea 559). Una vez hecho esto, podré liberar la versión 0.9.

  4. a continuación, migraré la barra de estado a KDE4, incluyendo el punto anterior (es por ello por lo que lo hago antes: como esa parte aún no ha sido migrada, el trabajo puede beneficiar tanto a la versión estable, como a la nueva).

  5. finalmente, terminaré las tareas restantes, en particular la tarea 560 (mejoras al botón “Atrás”), y la tarea 562 (mostrar en la barra de estado el número total de pistas, y puede que su duración también).

29-02-2008
Playlist (5): haciendo eficiente, casi por gusto, la cola de reproducción

Minirok hereda de Amarok el concepto de cola de reproducción: una manera de indicar qué pistas se han de reproducir a continuación, si no se desea seguir el orden propio de la lista (screenshot).

La implementación en la versión anterior de Minirok era bien sencilla: una lista de Python que contiene, en orden, los ítems según han sido encolados. Cuando acaba una pista, antes de pasar a reproducir el siguiente ítem, se intenta reproducir el primer ítem de la cola, si lo hubiere. Al dibujar cada pista, se recorre la lista (mediante su método index()) para ver si dicha pista es parte de la cola o no, y poder mostrar en la interfaz su posición. Para encolar o desencolar ítems, un único método toggle_enqueued(), capaz actuar sobre un ítem cada vez.

Lo que salta a la vista con esta implementación es que el dibujado de cualquier ítem es O(m), con m el tamaño de la cola, ya que para cada ítem hay que recorrer la cola para ver si el ítem se encuentra en ella o no. Esto suena, en principio, mal — sin embargo, y dado que el tamaño de la cola suele ser muy muy pequeño, el efecto es despreciable.

No obstante, al estar rescribiendo partes importantes del código, me pareció que era una buena oportunidad para cambiar esto. El resultado: una implementación más eficiente (y algo más compleja, dicho sea de paso), y la satisfacción de un trabajo bien hecho.


La primera mitad de la idea es sencilla y no nueva: como lo que hay que optimizar es la operación index() (que, dado un valor, obtiene su posición en la lista), hacemos de estos índices un valor calculado en los objetos de la clase PlaylistItem, siendo el modelo el encargado de mantenerlos actualizados. Es decir, la misma idea que en esta entrada comenté para la lista en sí.

En segundo lugar, y ya casi por ejercicio (imaginando que se tratara de una parte mucho más crítica en cuanto a eficiencia) he introducido un nuevo método toggle_enqueued_many(), capaz de encolar y desencolar varios ítems a la vez. La principal ventaja consiste en que en el caso de desencolar ítems de una lista muy grande, sólo hay que recalcular los atributos queue_position de los ítems restantes una vez.

Y, otra vez, me ha pasado que releyendo el código para comentarlo aquí me ha venido la inspiración, y he visto una manera más sencilla y eficiente de hacerlo. (De hecho, no entiendo muy bien cómo no lo hice así desde el principio, pues el problema es bien sencillo.)


Si nos dan una lista de índices a eliminar, y queremos ajustar los valores restantes de la lista de manera que coincidan con sus nuevas posiciones, la mejor manera (hasta donde yo he llegado :-) es agrupar los índices a eliminar en grupos contiguos, y decrementar los valores que quedan entre cada par de grupos (más entre el último grupo y el final de la lista) tantas unidades como ítems se hayan eliminado hasta ese punto.

Con un ejemplo se ve mejor. Por ejemplo, dada la lista:

  [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16,
   17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31]

Y la lista de índices a eliminar:

  [2, 10, 11, 12, 13, 14, 15, 21, 22, 23, 24, 25, 29]

Al agrupar esta última tenemos (el primer valor es la posición y el segundo la cantidad de ítems a eliminar):

  [(2, 1), (10, 6), (21, 5), (29, 1)]

Así, haríamos:

En código Python, por si a alguien le pica la curiosidad, viene a ser esto:

  def dequeue(self, indexes):
      indexes.sort()

      chunks = contiguous_chunks(indexes)
      chunks.append((len(self.queue), 0)) # fake chunk at the end

      accum = 0
      for i, (index, amount) in enumerate(chunks[:-1]):
          accum += amount
          until = sum(chunks[i+1])
          for item in self.queue[index+amount:until]:
              item.queue_position -= accum

      for index in reversed(indexes):
          item = self.queue.pop(index)
          item.queue_position = None

Casualmente, la función contiguous_chunks() la tenía escrita ya como parte del AlterItemlistMixin que comenté el otro día.

El diff entero, de la revisión 575 recién salida de la cocina, aquí.

28-02-2008
Playlist (4): hacer y deshacer

Una de las funcionalidades que resultaba muy difícil implementar en la versión anterior de Minirok era hacer y deshacer: al no estar las estructuras internas de la lista bajo nuestro control, tendríamos que haber añadido unas estructuras paralelas que mantener sincronizadas y que reflejaran los distintos estados por los que pasara la lista.

Con Qt4, y al usar el paradigma modelo/vista, sabía que implementar hacer y desacer para la lista tenía que ser al menos posible, sobre todo si (como he hecho) tenía en cuenta este requerimiento desde el principio. Lo que no me esperaba era que fuera tan (relativamente) fácil, gracias al Undo Framework de Qt4. En este modelo, cada modificación al estado de la lista se representa por un objeto, que sabe cómo deshacer su acción, y cómo rehacerla. Qt se encarga de todo lo demás.


Como expliqué en una de las entradas anteriores, el modelo ofrece dos métodos insert_items() y remove_items() para realizar modificaciones a la lista. Estas funciones son de funcionalidad limitada: por ejemplo sólo se pueden eliminar ítems contiguos en una misma llamada, y no tocan para nada la cola de reproducción.

Así, los objetos que representan alteraciones a la lista serán finos wrappers alrededor de estas dos funciones, con código para handlear los dos casos mencionados en el párrafo anterior.

Comencé con dos clases, InsertItemsCmd y RemoveItemsCmd, ambas derivadas de QUndoCommand. Como su funcionalidad es simétrica (es decir, el undo() de una hace lo mismo que que el redo() de la otra, y viceversa), he factorizado todo el código a un mixin, dejando la implementación de las clases comando en algo tan simple como:

  class InsertItemsCmd(QtGui.QUndoCommand, AlterItemlistMixin):
      undo = AlterItemlistMixin.remove_items
      redo = AlterItemlistMixin.insert_items

  class RemoveItemsCmd(QtGui.QUndoCommand, AlterItemlistMixin):
      undo = AlterItemlistMixin.insert_items
      redo = AlterItemlistMixin.remove_items

Con estas dos clases es posible representar cualquier modificación a la lista, incluido el drag and drop interno, creando una macro de dos comandos (es decir, una unidad indivisible): un RemoveItemsCmd primero, y un InsertItemsCmd que inserta los ítems eliminados por el primero.


Hay un caso particular de operación: eliminar todos los ítems de la lista (“clear playlist”). Aunque se puede expresar en términos de un RemoveItemsCmd (y una sola llamada a remove_items() en el modelo), hacerlo así resulta un poco ineficiente (ya que estas funciones iteran sobre todos los items eliminados).

Como es muy deseable que esta operación sea rápida, he creado una nueva función en el modelo, clear_itemlist(), como caso particular de remove_items(), pero más eficiente. Asimismo, he creado un nuevo comando ClearItemlistCmd, que hereda del mixin pero que proporciona su propia implementación de remove_items() (el método insert_items() original se conserva, pues no necesita modificaciones).

Esto último lo he hecho hoy mismo en la revisión 560 (diff).

22-02-2008
Playlist (3): elementos de visualización

La implementación por defecto de las vistas de ítems de Qt4, por ejemplo QTreeView, se limita a obtener los datos a mostrar mediante el método data() del modelo, y a actualizar las filas mostradas cuando el modelo indica que sus datos han cambiado. Dichos datos suelen ser texto o números, y se muestran sin ningún tipo de formato.

Para la lista de reproducción, sin embargo, queremos cambiar esa visualización por defecto en varias cosas:

Para el último punto, la solución idónea es una clase delegada, que son un mecanismo que ofrece la arquitectura modelo/vista de Qt4 para (entre otras cosas) refinar cómo han de mostrarse las celdas en la vista. En su versión más simple, una clase delegada tiene un único método paint(), que recibe un rectángulo de pantalla donde pintar, y los datos a pintar. Estos datos incluyen un puntero al modelo, y el número de fila y columna, por lo que es fácil preguntar al modelo si esta fila tiene una posición en la cola, o es la fila de “stop after”.

En ambos casos, estos elementos gráficos adicionales sólo hay que mostrarlos en la columna con el número de pista. Las clases vista permiten, con el método setItemDelegateForColumn() asignar una clase delegada sólo a cierta columna.

El código para pintar es prácticamente idéntico al presente en la versión anterior de Minirok; únicamente, pasa de estar en la clase PlaylistItem a la clase PlaylistTrackDelegate.


En una vista con celdas, las clases delegadas (por defecto o proporcionadas por el usuario) son invocadas celda a celda, por lo que no son idóneas para elementos visuales que afectan a toda la fila (p.ej. las dos primeras en la lista anterior).

En este caso, se puede proporcionar en la clase de la vista una implementación del método virtual drawRow() que implemente esos elementos. Esta implementación propia tendrá la siguiente forma:

  def drawRow(self, painter, styleoption, index):
      row = index.row()
      model = self.model()

      if model.row_is_playing(row):
          styleopt = QtGui.QStyleOptionViewItem(styleopt) # make a copy
          styleopt.font.setItalic(True)

      QtGui.QTreeView.drawRow(self, painter, styleopt, index)

      if model.row_is_current(row):
          # dibujar el borde...
21-02-2008
Playlist (2): estructura interna de los datos (y moraleja)

La lista de reproducción es, a todas todas, un simple array de ítems. Con Qt3, el orden de esta lista se guardaba en las estructuras internas (y no directamente accesibles) de la clase QListView. Esta clase ofrecía iteradores para acceder a los ítems en orden, y estos ítems tenían métodos como itemAbove() e itemBelow() para, dado por ejemplo la pista actual en reproducción, poder saltar a la pista siguiente o la anterior.

Ahora, con Qt4, la responsibilidad de estas estructuras internas corresponde al modelo, es decir, a nosotros mismos. Como implementadores, tenemos que almacenar la lista en una estructura o estructuras que, por una parte, proporcionen toda la funcionalidad necesaria y, por otra, sean razonablemente eficientes. Para ello, hay que saber primero qué tipo de acceso a la estructura vamos a necesitar.

Se necesita, en primer lugar, un acceso por índice numérico, y se necesita que sea rápido. Como se vio en la entrada anterior, el método data() del modelo recibe una fila y una columna; y dicho método se invoca muchas muchas veces por la vista, así que debe de ser realmente rápido. Asimismo, todo el resto de interacciones entre la vista y el modelo se basan en número de fila. Por esto, el almacén principal de los ítems es una lista de Python, llamada _itemlist, y que se debe modificar únicamente mediante dos métodos insert_items() y remove_items():

  class Playlist:

      def insert_items(self, position, items):
          amount = len(items)
          self.beginInsertRows(QtCore.QModelIndex(),
                               position, position + amount - 1)
          self._itemlist[position:0] = items
          self._row_count += amount
          self.endInsertRows()

      def remove_items(self, position, amount):
          self.beginRemoveRows(QtCore.QModelIndex(),
                               position, position + amount - 1)
          self._itemlist[position:position+amount] = []
          self._row_count -= amount
          self.endRemoveRows()

          return items

(Las funciones beginRemoveRows() y endRemoveRows() son funciones heredadas de la clase base, y han de ser llamadas para indicar a la vista que va a haber modificaciones en la lista de ítems.)


Por otra parte, en distintas partes el modelo también necesita la operación inversa: a partir de un determinado ítem, averiguar su posición. En principio esto se podría hacer con el método index() de las listas de Python, pero tendría complejidad O(n); y a nosotros nos conviene O(1), sobre todo con vistas a listas grandes (siendo n el tamaño de la lista).

Mi primera idea, que implementé y mantuve hasta ayer mismo, fue acoplar un diccionario a la lista de ítems. Es decir, mantener una estructura adicional que mapeara ítems a posiciones: la obtención de la posición de cualquier ítem sería O(1), a cambio de incrementar la complejidad temporal de las dos funciones explicadas anteriormente, para mantener el diccionario actualizado. (Este incremento, lamentablemente, es O(m+n) tanto en el caso peor como en el mejor, ya que hay que iterar sobre todos los elementos del diccionario y mirar si hay que actualizarlos uno a uno.)


Sin embargo, al comenzar a preparar esta entrada, lo miré todo con nuevos ojos, y se me ocurrió una manera más sencilla y eficiente: mantener un atributo con la posición en los ítems mismos.

Al principio de comenzar a migrar la lista, me planteé los ítems como un simple contenedor para los tags del fichero, y que todo el resto de información debería almacenarse en el modelo en sí. Y, por tanto, implementé el diccionario acoplado. Pero algo más tarde cambié de idea y comencé a incluir algunos otros atributos en los ítems, de cuya actualización el encargado era el modelo. Sin embargo, se me olvidó considerar este nuevo paradigma para almacenar la posición de cada ítem en la lista.

Una vez hecho, para mantener el atributo position actualizado sólo hay que añadir un poco de código a los dos métodos anteriores, así:

  class Playlist:

      def insert_items(self, position, items):
          [...]
          for item in self._itemlist[position:]:
              item.position += amount

          for i, item in enumerate(items):
              item.position = position + i
          [...]

      def remove_items(self, position, amount):
          [...]
          for item in items:
              item.position = None

          for item in self._itemlist[position+amount:]:
              item.position -= amount
          [...]

Este nuevo cambio tiene complejidad O(n+m) en el caso peor, pero O(m) en el caso mejor (siendo m el número de items a insertar o eliminar). El cambió está implementado en la revisión 549 (diff).


Moraleja: siempre es bueno examinar tras un tiempo las soluciones que hemos dado a un problema, por ejemplo describiendo por escrito sus detalles. Muy a menudo encontraremos que pasado un tiempo nuestro cerebro es capaz de ver maneras más eficientes o elegantes de hacer las cosas.

20-02-2008
Playlist (1): arquitectura modelo/vista

La versión 4 de Qt (el toolkit gráfico en el que se basa KDE4) introduce como una de sus funcionalidades estrella una arquitectura modelo/vista denominada Interview para los widgets que muestran una lista de ítems, como es el caso de una lista de reproducción.

Brevemente, esta arquitectura de Qt se basa en el patrón de diseño MVC (modelo/vista/controlador), que consiste en la separación de los datos en sí, y toda la lógica para su modificación (modelo), del código encargado de su visualización (vista).

Qt4 no hace obligatorio de este patrón, y proporciona un grupo de clases de conveniencia que se usan de manera muy similar a sus equivalentes en Qt3 (tal y como se usan en la rama estable de Minirok). Así, para la vista en árbol he utilizado una de estas clases de conveniencia, pero para la lista de reproducción me pareció que el uso de este patrón podría ser muy beneficioso (como se verá en próximas entradas).

Qt4 hace la interacción entre la vista y el modelo muy sencilla: el modelo sólo tiene que implementar unos pocos métodos virtuales, y con ellos la vista es capaz de mostrar toda la información sin necesitar código adicional por nuestra parte. Una visión simplificada de estos métodos imprescindibles sería:

  class Playlist(QtCore.QAbstractTableModel):

      def rowCount(self, parent):
          return len(self._items)

      def columnCount(self, parent):
          return len(self._column_names)

      def data(self, index, role):
          if (role == Qt.DisplayRole):
              return self._items[index.row()].tag_by_index(index.column())

      def headerData(self, section, orientation, role):
          if (role == Qt.DisplayRole
                 and orientation == Qt.Horizontal):
              return self._column_names[section]

En una entrada posterior explicaré cómo la vista se comunica con el modelo para realizar las acciones solicitadas por el usuario.

19-02-2008
Febrero, mes central del proyecto

Creo que, sin duda, febrero está siendo y va a ser el mes central de este proyecto. Después de terminar de migrar a KDE4 la vista en árbol del sistema de ficheros, el 30 de enero comencé a migrar la lista de reproducción. Desde entonces, y aprovechando el hueco libre dejado tras terminar los exámenes y el comienzo, siempre ligero, del nuevo cuatrimestre, he dedicado al proyecto muchas muchas horas, probablemente más o a la par que las horas dedicadas hasta ese día. Y aún no he terminado. :-)

La lista de reproducción es prácticamente la parte central de Minirok. Si miramos el código de la versión estable, el fichero playlist.py supone el 32% de todo el código escrito. Así, al migrar a KDE4 es normal que esta parte suponga una gran parte del tiempo dedicado.

Sin embargo, cuando digo “mes central” en el título no hablo únicamente en términos de tiempo: la migración a KDE4 ha supuesto una oportunidad única para rediseñar ciertos aspectos nucleares del código, permitiendo la implementación de nueva funcionalidad que antes no era posible o que se hubiera hecho difícil, o manteniendo funcionalidad pero ganando en rendimiento.

En los próximos días voy a escribir un número de entradas para ilustrar a qué me refiero, con vistas a proporcionar una visión más cercana del trabajo que he estado realizando. Entre otras cosas, hablaré de:

Todo el trabajo lo he estado realizando en la rama kde4_playlist del repositorio, y se puede ver un log de todos los cambios al fichero playlist.py aquí (comenzando en la revisión 416, y ya van más de 120 commits realizados).

18-02-2008
He migrado el blog a ikiwiki

He cambiado el software que usaba para generar este blog: de Hobix me he mudado a Ikiwiki. A finales de diciembre hice el mismo cambio para mi blog personal, y ahora me costaba mucho escribir entradas en este blog pues ya me había acostumbrado a la sintaxis por defecto de Ikiwiki (Markdown en lugar de Textile).

Ambos, Hobix e Ikiwiki, son generadores estáticos, es decir, no hace falta ningún tipo de permiso para ejecutar CGIs o lenguajes dinámicos en el servidor.

Ikiwiki es en realidad un “compilador de wikis”, pero incluye funcionalidad para generar blogs y sitios web estáticos en general.

18-02-2008
Oops, minirok 0.8.1

Hoy he liberado Minirok 0.8.1, para corregir un pequeño error que no pude detectar en la fase de pruebas antes de sacar 0.8.

Ah, y se me olvidó decir en el post anterior que he estado preparando paquetes de PyKDE4 para Debian, ya que estoy involucrado en el equipo encargado, y nadie lo estaba haciendo. Se puede ver el repositorio SVN aquí.

29-01-2008
Minirok 0.8 (y más)

Hoy he liberado Minirok 0.8; lo he anunciado en Freshmeat y KDE-Apps, y he subido a Debian la nueva versión.

La principal mejora es el refrescado rápido de la vista en árbol que implementé en diciembre, el diálogo para seleccionar el directorio a cargar, y la tarea 563, que en diciembre implementé la mitad, y ayer, para descansar un poco de la migración a KDE4, terminé de pulir (el código, aquí). La lista completa de cambios es ésta.

En cuanto a la migración a KDE4, he estado dedicándole bastante tiempo: ya tengo migrada por completo la vista en árbol, y esta semana que empieza me pondré con la lista de reproducción. Voy más lento de lo que me gustaría por lo que ya expliqué en el post anterior, que de vez en cuanto me encuentro bugs en las librerías que uso, y hay que debuguearlos (por ejemplo, éste). Por otra parte, ya he podido subir mi trabajo en esto al repositorio Subversion, se pueden ver todas las ramas en las que he estado trabajando en el directorio /branches.

27-01-2008
Portando a KDE4, que es gerundio

He empezado a migrar Minirok a KDE4 hace unos días. Esto incluye re-escribir gran parte del código de interfaz de usuario, adaptándolo a las nuevas interfaces del API de KDE.

Es un proceso, las cosas como son, lento y un poco laborioso, y sobre todo pesado. Le echo un par de meses. El mayor problema, sin embargo, es cuando te encuentras con bugs en esta nueva versión de las librerías, y tienes que trackearlos: crear un pequeño programa de prueba para demostrar que no es problema tuyo sino del código de KDE, enviarlo al autor original del código por mail, o pillarle en IRC, etc. Por ejemplo, este bug en las librerías PyKDE, y otro en las mismas kdelibs, descubierto y arreglado hoy mismo. Pero alegría, que es pesado pero así es cómo progresa el software libre, así que estoy contento. Y KDE4 trae muchas cosas buenas, so todo bien.

Estoy realizando el trabajo en una rama aparte en el repositorio, para poder seguir sacando nuevas versiones basadas en KDE3 mientras aún no he terminado la migración (la versión 0.8 está a la vuelta de la esquina!). Aún no he subido esta rama a Subversion porque hay un bug en bzr-svn, pero está en Bazaar aquí, y los commits van, como siempre, a la lista de commits.

10-01-2008
Inagurando el año

Bueno, el final de 2007 ha sido un poco caótico y con poco tiempo para Minirok, debido a que un familiar cercano tuvo que ser operado de urgencia y estuvo en el hospial (pero ya está bien!).

Ahora que ya ha pasado, he vuelto al tema. Ayer antes de irme a la fiesta de Nochevieja estuve trabajando en la tarea 724, que consiste en leer las etiquetas ID3 en un hilo de ejecución separado para aumentar la responsividad de la interfaz si se leen las etiquetas de un sistema de ficheros remoto lento, por ejemplo vía wireless. Conseguí implementarlo sin demasiado problema, pero tras hacerlo he visto que también habrá que hacer lo mismo para la lectura de los contenidos de los directorios, que también ralentizan la interfaz. Y eso, tras estar peleándome toda la tarde de ayer, no lo pude conseguir: os.listdir() se me bloquea, incluso si lo ejecuto en un hilo aparte.

Para desbloquearme un poco hoy, he decidido cambiar de contexto e realizar una tarea distinta, la 561, parpadeo de las etiquetas de posición durante la pausa. Ya está. \o/

Ah, también implementé hace un par de semanas la mejora de la que hablaba en la entrada anterior, el diálogo para seleccionar el directorio a cargar.

01-01-2008
Lista de correo para los commits, y más

Como en la Forja se crea automáticamente una lista de correo para los commits que realicemos (cosa bastante común en los proyectos de software libre), he decidido que estaría bien usarla. Como comento en el primer mensaje (algún admin debería revisar los problemas de encoding), para enviar los commits desde el repositorio Subversion hace falta accesso a la configuración del mismo, cosa que no tenemos, así que he decidido enviarlos desde mi repositorio Bazaar mediante un programa que escribí este verano.

Justo ahora mismo acabo de enviar emails para todos los commits que he realizado desde el comienzo del concurso. Mailman se ha confundido un poco con la fecha de cada email, pero bueno, ya todos los demás se enviarán con normalidad. La lista, aquí.

Ah, también he arreglado un bug que afectaba a la lectura de ficheros sin etiquetas ID3, y se me ha ocurrido otra mejora que introducir: un diálogo para seleccionar el directorio a mostrar, en lugar de tener que escribirlo a mano.

¡Esto va viento en popa!

12-12-2007
Inspirado... inspiradísimo!

Bueno, bueno, esto sí que no me lo esperaba. Después de una semana sin casi tiempo, y sin tenerlo planificado, ayer viernes me vino la inspiración sobre cómo implementar una de las tareas a las que le tenía más miedo o respeto, el refrescado rápido de la vista en árbol del sistema de ficheros.

Debe ser verdad eso que dicen que a la hora de programar nuestro cerebro es capaz de trabajar durante días o semanas en background, y cuando de repente nos aparece por la mente una solución, en realidad es porque llevamos tiempo meditándola inconscientemente.

El caso es que ya está: entre ayer y hoy, Minirok ya tiene refrescado rápido del árbol. La idea es sencilla, pero la implementacion es cuidadosa ya que hay que conjugar varias partes del código, como se puede apreciar en el changelog. El detalle de cambios, aquí.

Con todo en caché, Minirok es capaz de refrescar mi colección de 70 GB en apenas un segundo (una única llamada al sistema stat por directorio). ¡Esto casi se merece sacar una nueva versión!

08-12-2007
En esta última semana...

En esta última semana no he encontrado demasiado tiempo para dedicar a Minirok. Entre otras cosas, he estado en Mérida en una reunión de desarrolladores de Debian. Link.

Ayer realicé la mitad de la implementación de tarea 563, pero aún no he commiteado los cambios en el SVN, y hoy he podido completar la tarea 691, que no estaba reflejada en el planning que adjunté con mi inscripción, ya que se me ocurrió a finales de Octubre.

06-12-2007
Importando el repositorio Subversion

Acabo de importar mi repositorio Bazaar de Minirok al repositorio Subversion de la Forja. Esto es, he creado allí una copia con toda la historia. Como se puede ver por las fechas, los cambios que cuentan para el concurso comienzan en la revisiónn 266. La herramienta que he utilizado para esta tarea ha sido bzr-svn, que permite integrar repositorios Subversion y Bazaar de manera muy sencilla.

29-11-2007
Creando las tareas

Acabo de introducir en el gestor de tareas de la forja todas las tareas que mencioné en mi inscripción al concurso, más algunas más que se me han ido ocurriendo. Se pueden consultar aquí.

22-11-2007
Minirok 0.7 liberado

Acabo de liberar una nueva versión de Minirok, Minirok 0.7. El código fuente y paquetes para Debian y Ubuntu se pueden descargar tanto en la forja como en la página web del proyecto. También he subido la nueva versión a los repositorios de Debian, con lo que deberían estar disponibles para Ubuntu Hardy en breve. La lista entera de cambios se puede consultar aquí.

La mayoría de estos cambios los terminé de preparar justo antes de comenzar el concurso, excepto la compatibilidad con la última versión de una de las librerías que utilizo, lastfmsubmitd, que he preparado durante esta tarde. En esta última versión de la librería han introducido cambios en el API, por lo que he tenido que hacer los cambios pertinentes. Cambios que me han llevado más tiempo del que me esperaba, no sólo porque he querido mantener compatibilidad con la versión anterior, por si alguien instala Minirok en un sistema más antiguo, sino porque he acabado debugueando y encontrando un bug en la librería (éste).

Por último, mencionar que los cambios los he preparado en el repositorio Bazaar del proyecto (URL), y que los subiré al repositorio Subversion de la forja en cuanto los administradores puedan hacer un pequeño ajuste en la configuración del mismo que he solicitado. (Para los curiosos, pienso trabajar utilizando bzr-svn, que es capaz de crear repositorios Subversion a partir de repositorios Bazaar, de manera que resulten idénticos. El cambio en la configuración que he solicitado consiste en la activación del hook pre-revprop-change del repositorio Subversion en la forja, para que los commits en dicho repositorio tengan fechas correctas, y no haya confusión respecto a qué se añadió antes del comienzo del concurso, y qué después.)

21-11-2007
Anunciando el blog

Éste será el blog que utilizaré durante el desarrollo de Minirok como parte del II Concurso Universitario de Software Libre.

13-11-2007
Este blog está creado con ikiwiki.