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.
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!
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!
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.
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.
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.
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.
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 }
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. |
Michael Schilliarbeitet 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. |