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