Trainierter DJ (Linux-Magazin, Juli 2004)

Ein Perl-Skript mit Gtk-Oberfläche, integriertem MP3-Spieler und Datenbank lädt dazu ein, Stücke aus der Musiksammlung nach verschiedenen Kriterien zu bewerten. Anschließend spielt der virtuelle DJ reihenweise nur Stücke nach Geschmack und Gemütsstimmung.

In einer MP3-Sammlung aus gerippten CDs finden sich immer wieder Überraschungen: Während man die Silberscheiben selten konzentriert von Anfang bis Ende hört, fördert eine computergesteuerte Zufallsauswahl aus tausenden von MP3-Dateien manchmal erstaunliche Kostbarkeiten zutage. Doch wer merkt sich schon, wo die Kleinode herumlungern?

Das heute vorgestellte Perl-Skript stellt während einer Lern-Phase die Stücke in zufälliger Reihenfolge vor und lässt den Benutzer nach zwei Kriterien abstimmen: Den von mir so benannten Energize- und dem Schmoop-Faktoren. Energize bestimmt die Dynamik des Stücks, Schmoop die Schmusigkeit. Auf einer Skala von 1 bis 5 hätte etwa der Song ``Thunderstruck'' von AC/DC einen Energize-Faktor von 5 und eine Schmoop-Faktor von 1. Am anderen Ende des Spektrums wäre etwa ``Don't Know Why'' von Norah Jones mit einem Energize-Faktor von 1 und einem Schmoop-Faktor von 5.

Jedes Mal, wenn der Benutzer seine Stimme abgibt, speichert eine Datenbank den MP3-Pfad zum Song und die beiden Faktoren. Haben sich so spielerisch einige Wertungen angesammelt, kann das Skript auf Anfragen wie ``Spiele mir schnelle Songs, aber keine, die meine Freundin verscheuchen'' Playlisten erstellen und diese stundenlang abspielen.

Abbildung 1: rateplay wurde instruiert, Energize-Levels 3 bis 5 mit bei niedrigst-Schmoop-Werten zu auszuwählen und spielt gerade einen Heavy-Metal-Song mit einem Energize-Level von 5 und einem Schmoop-Level von 1.

Die GUI in Abbildung 1 zeigt das Skript in Aktion: Zum Abspielen bereits bewerteter Songs klickt man akzeptable Energize- und Schmoop-Faktoren in den oberen zwei Knopfreihen an: Im Bild sind 3, 4 oder 5 für Energize erlaubt und 1, 2 oder 3 für Schmoop -- also energiegeladene Stücke für Junggesellenparties. Ein anschließender Mausklick auf ``Play Rated'' erstellt eine zufällige Playlist aus allen Songs in der Datenbank, die darauf passen und spielt diese der Reihe nach ab. Währenddessen kann man den Player mit ``Play Next'' oder ``Play Previous'' veranlassen, zum nächsten oder vorherigen Stück zu springen.

Um neue Stücke zu bewerten, klickt man auf ``Random Rate''. Dann erstellt das Skript eine Playlist aus bisher unbewerteten Songs und spielt diese der Reihe nach ab. Der Benutzer stellt dann jeweils die Bewertung des Songs in den am unteren Rand liegenden Radio-Buttons (denn nur ein Energize- bzw. Schmoop-Level pro Song ist erlaubt) ein und klickt den ``Rate''-Button am unteren Rand, um die Bewertung persistent in die Datenbank einzufüttern. Ein Klick auf ``Rate'' lässt das Skript gleich zum nächsten Song springen, der Benutzer kann aber auch mittels ``Play Next'' oder ``Play Previous'' herummanövrieren, ohne Wertungen abzugeben.

Vom Duo zum Trio

Das vorgestellte Skript rateplay zeigt die Implementierung. Zu dem aus [3] bekannten Erfolgsduo POE und Gtk für ruckfreie GUIs gesellt sich heute noch der Kommandozeilen-MP3-Spieler musicus von Robert Muth dazu, ein C++-Programm, das auf den dynamischen Libraries von xmms aufsetzt. Das Modul POE::Component::Player::Musicus von Curtis Hawthorne bindet den MP3-Spieler in den POE-Reigen ein -- so kann die GUI den Player ruckelfrei fernsteuern.

Die persistente Speicherung der Bewertungen erfolgt über die schon einmal in [5] vorgestellte objektorientierte Class::DBI-Abstraktion, die diesmal eine SQLite-Datenbank als Speichermedium nutzt. Dieses erstaunliche Produkt legt in einer einzigen Datei eine professionelle Datenbank mit SQL-Abfragemöglichkeit an. Natürlich gibt es dafür auch ein Perl-Modul aus der DBI-Reihe auf dem CPAN, und da SQLite unter einer Public-Domain-Lizenz steht, enthält das Modul den C-Code für die Datenbank gleich mit. Praktisch!

SQLite - Wolf im Schafspelz

Dabei ist SQLite eine ganz normale SQL-Datenbank. Um festzustellen, wieviele bewertete Songs in der definierten Tabelle rated_songs stehen, dockt man einfach mit dem Kommandzeilen-Tool sqlite an die von rateplay erzeugte und verwaltete Datenbankdatei rp.dat and und setzt einen SQL-Befehl ab:

    sqlite rp.dat
    SQLite version 2.8.12
    Enter ".help" for instructions
    sqlite> select count(*) from rated_songs;
    887

Aha. 887 Songs habe ich also schon bewertet. Noch ein Haufen Arbeit vor mir, aber es reicht schon, um erstaunliche Playlisten zu erstellen!

Zum Skript: Die Konfigurationszeilen 8 bis 10 definieren den Pfad zur Datenbankdatei ($DB_NAME) und ein Verzeichnis $SONG_DIR, in dem das nachstehend definierte find-Programm rekursiv nach *.mp3-Dateien sucht.

Die globalen Arrays @PLAY_ENERG und @PLAY_SCHMO speichern die Werte der CheckButtons für die Songauswahl am oberen Rand der GUI. Die Skalare $RATE_ENERG bzw. $RATE_SCHMO hingegen reflektieren die Einstellungen der Radio-Buttons am unteren Rand der GUI und nehmen Werte von 1 bis 5 für Energize und Schmoop-Faktor an. Die Arrays @RATE_ENERG_BUTTONS und @RATE_SCHMO_BUTTONS hingegen führen die Objekte der Radio-Buttons als Elemente, damit die GUI die für einen Song in der Datenbank gefundenen Werte gleich voreinstellen kann.

Die Klasse Rateplay::DBI ab Zeile 27 erbt von Class::DBI und definiert die OO-Abstraktion auf die SQLite-Datenbank. Falls diese noch nicht existiert (was bei SQLite einfach daran zu sehen ist, dass es die entsprechende Datei noch nicht gibt), legt der SQL-Code ab Zeile 36 einfach die Datenbank samt benötigter rated_songs-Tabelle mit den Spalten path (Pfad zur MP3-Datei), energize (Energize-Level) und schmoop (Schmoop-Level) an. Zeile 43 führt die Arbeit aus. Das in Zeile 30 hereingezogene Class::DBI::AbstractSearch erlaubt später über Class::DBI-Funktionalität hinausgehende AND/OR-Abfragen mit Rateplay::Song->search_where(). Die OO-Abstraktion auf die Tabelle legt die Klasse Rateplay::Song ab Zeile 47 an. Damit ist der Rest des Skripts ist SQL-frei!

Spieler tanzt den POE-Tango

Das Hauptprogramm steht im Paket main ab Zeile 56 und definiert die POE-Session, die die GUI und den Player betreibt. Der mit dem Parameter package_states referenzierte Array definiert eine Liste von weiter unten definierten Funktionen, die von gleichnamigen POE-Events ausgelöst werden. Ruft das Hauptprogramm beispielsweise die getpos()-Methode des Players auf, antwortet dieser mit der aktuellen Position im gerade gespielten Song, indem er der POE-Hauptsession einen getpos-Event schickt. Dank der obigen package_states-Definition weiß die Session nun, dass sie in diesem Fall die ab Zeile 78 definierte Funktion getpos anspringen muss. Ähnlich verhält es sich mit dem getinfocurr-Event: Gemäß der POE::Component::Player::Musicus-Manualseite wird er ausgelöst, wenn wenn man die getinfocurr()-Methode des Player-Objekts aufruft und gibt an die Callback-Funktion Interpret, Titel und mehr MP3-Informationen des gerade gespielten Stücks weiter. Die Zeilen 91 und 92 frischen dann die Interpret- und Titelanzeige in der GUI auf.

Den song-Event hingegen löst rateplay selbst aus: Immer wenn der Player ein neues Stück spielen soll, schickt das Skript wie in Zeile 306 einen song-Event an die in Zeile 142 so getaufte main Session:

    $poe_kernel->post('main', 'song', $path);

Dabei übergibt sie dem Event den Pfad der abzuspielenden MP3-Datei. Die main-Session ruft daraufhin die ab Zeile 96 definierte gleichnamige song-Funktion auf, die den Dateipfad als erstes Argument aufschnappt, den Player zuerst stoppt und dann sofort wieder mit der neuen MP3-Datei startet.

Der scan_mp3s-Event wird in Zeile 74 kurz nach dem Systemstart ausgelöst und lässt das Skript in die ab Zeile 106 definierte Funktion scan_mp3s springen. Diese holt nicht nur mittels retrieve_all() alle bewerteten Songs aus der Datenbank und legt sie als Schlüssel im globalen Hash %RATED ab, sondern startet auch noch einen Kindprozess in einer POE::Component::Child-Session, der das externe find-Kommando aufruft und MP3-Dateien auf der Festplatte aufstöbert.

Wann immer der Prozess eine Datei gefunden hat und eine Zeile nach STDOUT schreibt, springt die Session wegen der Event-Definition in Zeile 112 (und der package_states-Definition in Zeile 63) in die mp3_stdout-Funktion, die ab Zeile 327 definiert ist. Diese prüft, ob die Datei schon bewertet wurde. Falls nicht, hängt sie sie an den globalen Array @MP3S an, in dem die Pfade aller noch zu bewertenden MP3-Dateien stehen. Außerdem frischt Zeile 336 die Status-Anzeige über die ablaufende Suche auf.

Wie in [3] schon einmal ausgeführt, arbeitet POE mit merkwürdigen Parameter-Konstanten, ARG0 ist zum Beispiel der Index, an dem der Parameter in @_ steht, der dem Event übergeben wurde. Der von POE::Component::Child gesetzte erste Parameter ist eine Referenz auf einen Hash, der unter dem Schlüssel out die gerade aufgeschnappte STDOUT-Zeile des Kind-Prozesses parat hält.

Micro-Manager für den Spieler

Leider gibt POE::Component::Player::Musicus keinen Event ab, wenn ein gespieltes Stück beendet ist. Vielmehr lungert der Spieler tatenlos herum. Also muss rateplay ihn in kurzen Abständen nerven, und die aktuelle Lied-Position mit getpos() abfragen. Kommt ein negativer Wert zurück, dreht der Spieler gerade Däumchen und lechzt nach einer ABM-Maßnahme in Form eines neuen Songs. Dieses periodische ``Pollen'' realisiert die dem poll_player-Event explizit in den inline_states zugewiesene Funktion ab Zeile 68. Sie schickt nicht nur eine getpos()-Abfrage an den Player (deren Ergebnis später einen getpos-Event an die main-Session sendet), sondern sorgt mit

    $poe_kernel->delay('poll_player', 1);

auch noch dafür, dass der Kernel nach exakt einer Sekunde wieder einen poll_player-Event auslöst -- der Kreis des nervigen Micro-Managements schließt sich.

Die ab Zeile 78 definierte getpos-Funktion frischt die our-Variable $POS auf, die einen Integerwert speichert, der der gerade aktuellen Position im Song entspricht. War der vorherige Wert größer Null und kommt nun ein negativer Wert daher, ist dies ein Indiz dafür, dass der Player gerade einen Song beendet hat und Zeile 82 die next_in_playlist()-Funktion aufrufen kann.

Diese extrahiert das erste Element des globalen Arrays @PLAYLIST, schiebt es hinten wieder drauf und reicht es in Zeile 306 dem Player zum Abspielen weiter. Falls der erste Parameter von next_in_playlist gesetzt ist, geht die nächste Fahrt stattdessen rrrückwärts und der vorige Song wird nochmal gespielt. Ist das Resultat aufgrund kompensierenden Vor- und Rückwärtsfahrens wieder derselbe Song, fährt Zeile 301 nochmal Eins weiter.

Für jeden neu gespielten Song ruft der song-Eventhandler die ab Zeile 310 definierte Funktion update_rating() auf. Diese sieht mittels der search-Methode in der Datenbank nach, ob der Song schon bewertet wurde und besetzt die Radio-Knöpfe mit den entsprechenden Werten für energize und schmoop, falls sie ein Ergebnis findet. Falls nicht, stellt sie die kleinstmöglichen Werte ein. So kann man für jeden Song auf einer Playlist das Rating sehen und sogar korrigieren, indem man ein neues Rating einstellt und dann den ``Rate''-Knopf ganz unten drückt.

Geschmackvolle Auswahl

Die ab Zeile 271 definierte Funktion select_songs kümmert sich um die Auswahl von Songs für eine Playlist gemäß den in der GUI eingestellten Checkbutton-Werten für Energize- und Schmoop-Werte. Die Arrays @PLAY_ENERG und @PLAY_SCHMO führen jeweils fünf Elemente. Ist der zugeordnete Checkbutton am oberen Ende der GUI gedrückt, ist das Element 1, andernfalls 0. Steht in @PLAY_ENERG beispielsweise (0,0,1,1,0) sind der dritte und der vierte Checkbutton in der Energize-Leiste gedrückt, alle anderen sind deaktiviert. Zeile 273 extrahiert daraus die Liste der gewünschten Energize-Werte und legt sie in @energ ab: (3,4). Der search_where()-Aufruf in Zeile 280 fügt noch schnell einen ungültigen Wert 0 hinzu -- das nur, um zu verhindern, dass es zu einem Fehler kommt, falls @energ leer ist, denn search_where() reagiert auf leere Arrays allergisch. Beide Kriterien für Energize und Schmoop verknüpft search_where() mit einem logischen Und -- also äquivalent zu WHERE A AND B in SQL. Die Elementwerte der übergebenen Arrays hingegen werden als OR ausgewertet. So selektiert

    Rateplay::Song->search_where({
        energize => [2, 3, 0],
        schmoop  => [1, 0]});

entsprechend der SQL-Abfrage

    SELECT * from rated_songs 
    WHERE energize = 2 OR
          energize = 3 OR
          energize = 0
    AND   schmoop = 1 OR
          schmoop = 0

alle Songs in den angegebenen Geschmacksgrenzen. Das vor den map-Befehl gesetzte sort { rand < 0.5 } in Zeile 278 wirbelt die die Ergebnisliste durcheinander, bevor sie an den Player geht -- schließlich brauchen wir Abwechslung und nicht immer die dieselbe Spielfolge.

Die Funktion process_rating ab Zeile 260 sucht mit find_or_create zunächst einen Eintrag unter dem angebenen MP3-Pfad in der Datenbank. Sie gibt das gefundene Objekt zurück, falls es fündig wird. Falls nicht, erzeugt find_or_create praktischerweise einfach einen neuen Eintrag. Die energize()- und schmoop()-Methodenaufrufe setzen die entsprechenden Felder des Records und die update()-Methode schreibt anschließend alles in die Datenbank zurück.

Äußerlichkeiten

Die ab Zeile 137 definierte Funktion my_gtk_init erzeugt die Gtk-Oberfläche. Alle GUI-Objekte landen unter beschreibenden Namen im globalen Hash %GUI, damit sie schön gruppiert und einfach global zugreifbar sind, denn manche Funktion muss in bestimmten Situationen schnell einige GUI-Felder auffrischen. Wie in [3] kommen zwei verschiedene Container zum Einsatz, Gtk::VBox und Gtk::Table, beide mit verschiedenen Pack-Verfahren, pack_start() und attach_defaults().

Die ab Zeile 229 definierte Funktion add_buttons() wird für beide Checkbox-Button-Reihen am oberen Ende der GUI aufgerufen. Jedesmal schiebt das Hauptprogramm eine andere Funktion hinein, die aufgerufen wird, falls der Button selektiert oder deselektiert wird.

Ab Zeile 192 definiert rateplay, was passiert, wenn bestimmte Knöpfe gedrückt werden. Als Reaktion auf ein 'destroy'-Signal, das eintrifft, falls jemand das Applikationsfenster schließt, wird mit Gtk->exit(0) die GUI abgerissen. Der Knopf "Play Rated" ($btns[0]) löst select_songs() aus und stößt mit next_in_playlist() die Playlist an. Play Next und Play Previous fahren vor oder zurück und "Random Rate" ($btns[3]) wirbelt die im globalen Array @MP3S gespeicherten unbewerteten MP3s mittels der shuffle-Funktion aus Algorithm::Numerical::Shuffle durcheinander, um sie dann der Reihe nach anzubieten. Und der Rate-Button schließlich greift auf die in der Funktion getinfocurr gesetzte globale Variable $TAG zu, die das MP3-Tag des gerade gespielten Songs enthält und ruft process_rating() auf, um einen Datenbankeintrag für den Song entsprechend den eingestellten Radiobuttons vorzunehmen.

Listing 1: rateplay

    001 #!/usr/bin/perl
    002 ###########################################
    003 # rateplay - Rate MP3s and play them
    004 # Mike Schilli, 2004 (m@perlmeister.com)
    005 ###########################################
    006 use strict; use warnings;
    007 
    008 our $DB_NAME  = "/data/rp.dat";
    009 our $SONG_DIR = "/ms1/SONGS/pods";
    010 our $FIND     = "/usr/bin/find";
    011 
    012 use Gtk; use POE;
    013 use Class::DBI;
    014 use POE::Component::Player::Musicus;
    015 use Algorithm::Numerical::Shuffle 
    016                                qw(shuffle);
    017 my (%GUI, %RATED, $TAG, $SONG, @PLAYLIST, 
    018     @MP3S);
    019 my @PLAY_ENERG = (0, 0, 0, 0, 0);
    020 my @PLAY_SCHMO = (0, 0, 0, 0, 0);
    021 my $RATE_ENERG = 0;
    022 my $RATE_SCHMO = 0;
    023 my @RATE_ENERG_BUTTONS = ();
    024 my @RATE_SCHMO_BUTTONS = ();
    025 
    026 ###########################################
    027 package Rateplay::DBI;
    028 ###########################################
    029 use base q(Class::DBI);
    030 use Class::DBI::AbstractSearch;
    031 
    032 __PACKAGE__->set_db('Main',
    033   "dbi:SQLite:$main::DB_NAME", 'root', '');
    034 
    035 if(! -e "$main::DB_NAME") {
    036     __PACKAGE__->set_sql(create => q{
    037         CREATE TABLE rated_songs (
    038             path VARCHAR(256) 
    039                  PRIMARY KEY NOT NULL,
    040             energize INT, schmoop  INT
    041         )
    042     });
    043     __PACKAGE__->sql_create()->execute();
    044 }
    045 
    046 ###########################################
    047 package Rateplay::Song;
    048 ###########################################
    049 use base q(Rateplay::DBI);
    050 
    051 __PACKAGE__->table('rated_songs');
    052 __PACKAGE__->columns(
    053     All => qw(path energize schmoop));
    054 
    055 ###########################################
    056 package main;
    057 
    058 my $PLAYER = 
    059     POE::Component::Player::Musicus->new();
    060 
    061 POE::Session->create(
    062    package_states => [ "main" => [ 
    063      qw(getpos getinfocurr mp3_stdout
    064         song   scan_mp3s)]],
    065 
    066    inline_states => {
    067      _start      => \&my_gtk_init,
    068      poll_player => sub { 
    069       $PLAYER->getpos();
    070       $poe_kernel->delay('poll_player', 1);
    071    }});
    072 
    073 $poe_kernel->post("main", "poll_player");
    074 $poe_kernel->post("main", "scan_mp3s");
    075 $poe_kernel->run();
    076 
    077 ###########################################
    078 sub getpos {
    079 ###########################################
    080   our $POS;
    081 
    082   next_in_playlist() if defined $POS and 
    083        $POS > 0 and $_[ARG0] < 0;
    084   $POS = $_[ARG0];
    085 }
    086 
    087 ###########################################
    088 sub getinfocurr {
    089 ###########################################
    090     $TAG = $_[ARG0];
    091     $GUI{artist}->set($TAG->{artist});
    092     $GUI{title}->set($TAG->{title});
    093 }
    094 
    095 ###########################################
    096 sub song {
    097 ###########################################
    098     $SONG = $_[ARG0];
    099     $PLAYER->stop();
    100     $PLAYER->play($SONG);
    101     $PLAYER->getinfocurr();
    102     update_rating($SONG);
    103 }
    104 
    105 ###########################################
    106 sub scan_mp3s {
    107 ###########################################
    108   %RATED = map { $_->path() => 1 }
    109             Rateplay::Song->retrieve_all();
    110 
    111   my $comp = POE::Component::Child->new(
    112     events => { 'stdout' => 'mp3_stdout' },
    113   );
    114 
    115   $comp->run($FIND, $SONG_DIR);
    116 }
    117 
    118 ###########################################
    119 sub add_label {
    120 ###########################################
    121   my($parent, $text, @coords) = @_;
    122   
    123   my $lbl = Gtk::Label->new();
    124   $lbl->set_alignment(0.5, 0.5);
    125   $lbl->set($text);
    126 
    127   if(ref $parent eq "Gtk::Table") {
    128    $parent->attach_defaults($lbl, @coords);
    129   } else {
    130    $parent->pack_start($lbl, 0, 0, 0);
    131   }
    132 
    133   return $lbl;
    134 }
    135 
    136 ###########################################
    137 sub my_gtk_init {
    138 ###########################################
    139   my @btns = ("Play Rated", "Play Next",
    140            "Play Previous", "Random Rate");
    141 
    142   $poe_kernel->alias_set('main');
    143 
    144   $GUI{mw} = Gtk::Window->new();
    145   $GUI{mw}->set_default_size(150,200);
    146 
    147   $GUI{vb} = Gtk::VBox->new(0, 0);
    148 
    149   $GUI{$_}= Gtk::Button->new($_) for @btns;
    150 
    151   my $tbl = Gtk::Table->new(2, 6);
    152   $GUI{vb}->pack_start($tbl, 1, 1, 0);
    153 
    154   add_label($tbl, 'Energize', 0, 1, 0, 1);
    155   add_buttons($tbl,
    156     sub { $PLAY_ENERG[$_[1]] ^= 1 }, 0);
    157   add_label($tbl, 'Schmoop', 0, 1, 1, 2);
    158   add_buttons($tbl,
    159     sub { $PLAY_SCHMO[$_[1]] ^= 1}, 1);
    160 
    161         # Status line on top of buttons
    162   $GUI{status} = add_label($GUI{vb}, "");
    163 
    164       # Pack buttons
    165   $GUI{vb}->pack_start($GUI{$_}, 0, 0, 0) 
    166                                  for @btns;
    167 
    168   for(qw(artist title)) {
    169     $GUI{$_} = add_label($GUI{vb}, "");
    170   }
    171 
    172   $GUI{rate_table} = Gtk::Table->new(2, 6);
    173   $GUI{vb}->pack_start($GUI{rate_table}, 
    174                                   0, 0, 0);
    175 
    176   add_label($GUI{rate_table}, 
    177             'Energize', 0, 1, 0, 1);
    178   attach_radio_buttons($GUI{rate_table},
    179     sub { $RATE_ENERG = $_[1]+1;
    180         }, 0, \@RATE_ENERG_BUTTONS);
    181   add_label($GUI{rate_table}, 
    182             'Schmoop', 0, 1, 1, 2);
    183   attach_radio_buttons($GUI{rate_table},
    184     sub { $RATE_SCHMO = $_[1]+1;
    185         }, 1, \@RATE_SCHMO_BUTTONS);
    186 
    187   my $rate  = Gtk::Button->new('Rate');
    188   $GUI{vb}->pack_start($rate,  0, 0, 0);
    189   $GUI{mw}->add($GUI{vb});
    190 
    191       # Destroying window
    192   $GUI{mw}->signal_connect('destroy',
    193     sub {Gtk->exit(0)});
    194 
    195         # Pressing Play Rated button
    196   $GUI{$btns[0]}->signal_connect('clicked',
    197     sub { @PLAYLIST = select_songs();
    198       $GUI{status}->set("Playlist has " . 
    199         scalar @PLAYLIST . " songs.");
    200       next_in_playlist();
    201     });
    202 
    203       # Pressing Play Next button
    204   $GUI{$btns[1]}->signal_connect('clicked',
    205     sub { next_in_playlist() });
    206 
    207       # Pressing Play Previous button
    208   $GUI{$btns[2]}->signal_connect('clicked',
    209     sub { next_in_playlist(1) });
    210 
    211       # Pressing Random Rate Button
    212   $GUI{$btns[3]}->signal_connect('clicked',
    213     sub { @PLAYLIST = shuffle @MP3S;
    214       $GUI{status}->set("Random Rating " . 
    215              scalar @PLAYLIST . " songs.");
    216       next_in_playlist();
    217     });
    218         # Pressing Rate button
    219   $rate->signal_connect('clicked',
    220     sub { return unless defined $TAG;
    221           process_rating();
    222           next_in_playlist();
    223         } );
    224 
    225   $GUI{mw}->show_all();
    226 }
    227 
    228 ###########################################
    229 sub add_buttons {
    230 ###########################################
    231   my($table, $sub, $row) = @_;
    232 
    233   for (0..4) {
    234     my $b = Gtk::CheckButton->new($_+1);
    235     $b->signal_connect(clicked=> $sub, $_);
    236     $table->attach_defaults($b, 1+$_, 2+$_,
    237                            0+$row, 1+$row);
    238   }
    239 }
    240 
    241 ###########################################
    242 sub attach_radio_buttons {
    243 ###########################################
    244   my($table, $sub, $row, $buttons) = @_;
    245   my $group;
    246 
    247   for (0..4) {
    248     my $btn = Gtk::RadioButton->new($_+1, 
    249         defined $group ? $group : ());
    250     $group = $btn;
    251     $btn->signal_connect(clicked => $sub, 
    252                          $_);
    253     push @$buttons, $btn;
    254     $table->attach_defaults($btn, 1+$_, 
    255                      2+$_, 0+$row, 1+$row);
    256   }
    257 }
    258 
    259 ###########################################
    260 sub process_rating {
    261 ###########################################
    262   my $rec = Rateplay::Song->find_or_create(
    263                     { path     => $SONG });
    264 
    265   $rec->energize($RATE_ENERG);
    266   $rec->schmoop($RATE_SCHMO);
    267   $rec->update();
    268 }
    269 
    270 ###########################################
    271 sub select_songs {
    272 ###########################################
    273   my @energ = grep { $PLAY_ENERG[$_-1] } 
    274          (1..@PLAY_ENERG);
    275   my @schmo = grep { $PLAY_SCHMO[$_-1] } 
    276          (1..@PLAY_SCHMO);
    277 
    278   return sort { rand > 0.5 } 
    279          map { $_->path() } 
    280              Rateplay::Song->search_where({
    281                energize => [@energ, 0],
    282                schmoop  => [@schmo, 0]},
    283   );
    284 }
    285 
    286 ###########################################
    287 sub next_in_playlist {
    288 ###########################################
    289   my($backward) = @_;
    290 
    291   return unless scalar @PLAYLIST;
    292   my $path;
    293 
    294   { if($backward) {
    295       $path = pop @PLAYLIST;
    296       unshift @PLAYLIST, $path;
    297     } else {
    298       $path = shift @PLAYLIST;
    299       push @PLAYLIST, $path;
    300     }
    301     redo if defined $SONG and 
    302           $SONG eq $path and @PLAYLIST > 1;
    303   }
    304 
    305   $PLAYER->stop();
    306   $poe_kernel->post('main', 'song', $path);
    307 }
    308 
    309 ###########################################
    310 sub update_rating {
    311 ###########################################
    312   my ($path) = @_;
    313 
    314   if(my ($song) = Rateplay::Song->search(
    315                           path => $path)) {
    316     my $e = $song->energize();
    317     my $s = $song->schmoop();
    318     $RATE_SCHMO_BUTTONS[$s-1]->activate();
    319     $RATE_ENERG_BUTTONS[$e-1]->activate();
    320   } else {
    321     $RATE_SCHMO_BUTTONS[0]->activate();
    322     $RATE_ENERG_BUTTONS[0]->activate();
    323   }
    324 }
    325 
    326 ###########################################
    327 sub mp3_stdout {
    328 ###########################################
    329   my ( $self, $args ) = @_[ ARG0 .. $#_ ];
    330 
    331   return if exists $RATED{$args->{out}};
    332 
    333   push @MP3S, $args->{out};
    334 
    335   $GUI{status}->set(scalar @MP3S . 
    336               " songs ready for rating.");
    337 }

Installation

Damit rateplay mit dem MP3-Player zusammenspielt, installiert man das xmms-1.2.10 RPM von http://www.xmms.org und lädt den musicus-Tarball von [2] herunter, entpackt ihn und tippt make. So entsteht ein musicus-Binary, das nach /usr/bin wandert.

Die Perl-Module POE, POE::Component::Player::Musicus und Gtk finden sich auf dem CPAN, [3] gibt einige Hinweise, falls die Installation Schwierigkeiten macht. Weiter werden DBI, DBD::SQLite, Class::DBI Class::DBI::AbstractSearch und Algorithm::Numerical::Shuffle benötigt. Die CPAN-Shell löst alle eventuell auftretenden Abhängigkeiten automatisch auf.

musicus und POE::Component::Player::Musicus werden eifrig weiterentwickelt. Falls die gerade aktuellen Versionen nicht zusammen ticken, stehen unter http://perlmeister.com/musicus zwei Tarbälle bereit, die funktionieren.

Abbildung 2: Die rated_songs-Tabelle mit den subjektive Wertungen Stücke aus der Plattensammlung des Michael S.

Abbildung 3: Der POE-Zustandsautomat springt zwischen verschiedenen Zuständen hin und her und hält dabei die Gtk-GUI in Schuss. Die PoCo-Kästen abstrahieren parallel laufende Prozesse mit eigenen Zustandsautomaten.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2004/07/Perl

[2]
Musicus-Homepage http://muth.org/Robert/Musicus

[3]
``Kurs-Makler'', Michael Schilli, Linux-Magazin 04/2004, Perl-Anwendung mit POE und Gtk, http://www.linux-magazin.de/Artikel/ausgabe/2004/04/perl/perl.html

[4]
``Überall Musik'', Michael Schilli, Linux-Magazin, 08/2003, http://www.linux-magazin.de/Artikel/ausgabe/2003/08/perl/perl.html

[5]
``Musik aus dem Keller'', Michael Schilli, Linux-Magazin, 03/2003, http://www.linux-magazin.de/Artikel/ausgabe/2003/03/perl/perl.html

[6]
``An Embeddable SQL Database Engine'', http://sqlite.org

Michael Schilli

arbeitet als Software-Engineer bei Yahoo! in Sunnyvale, Kalifornien. Er hat "Goto Perl 5" (deutsch) und "Perl Power" (englisch) für Addison-Wesley geschrieben und ist unter mschilli@perlmeister.com zu erreichen. Seine Homepage: http://perlmeister.com.