Mit den Perl-Modulen Class::DBI und CDDB_get wandern CD-Daten mit Songinformationen ohne Tipparbeit in eine Datenbank.
Mit meiner CD-Sammlung führe ich große Pläne im Schilde: In naher Zukunft werden alle zu MP3s gerippt, auf einen Server gestellt und dann nur noch mittels Geräten wie dem Slinky SliMP3 [2] auf der Stereo-Anlage abgespielt.
Bevor's an Einmotten der Silberscheiben geht, hätte ich deren Titel-, Interpreten- und Songinformationen jedoch gerne in einer Datenbank, die ich dann, wenn die Scheiben im Keller sind, mit Fragen wie ``In welchem Schuhkarton liegt nochmal die Foo-Figher-CD, auf der der Song 'Generator' ist, dessen MP3 ich gerade versehentlich gelöscht habe?'' traktieren kann.
Da sich das manuelle Abtippen dieser Informationen bis ca. Mai 2007 hingezogen hätte, beschloss ich, ein kleines Skript zu schreiben, das eine gerade im Laufwerk liegende CD einliest, den freien CDDB-Server auf http://freedb.freedb.org kontaktiert, und die dort gefundenen Daten sofort in meiner eigenen Datenbank ablegt.
Und, in letzter Zeit hat das neue Datenbank-Modul Class::DBI
(Original von Michael Schwern, heute unter der Fuchtel von Tony Bowden,
siehe [3])
ziemlich Furore
gemacht: Es entkoppelt den datenbanküblichen SQL-Wirrwarr endlich sauber
von den Perl-Applikationen. Also spannte ich es kurzerhand
für die Arbeit ein.
Heute nutzen wir zwei Tabellen:
cds
tracks
cds
gelisteten CD.
Den Bezug zur cds
-Tabelle stellt
jeweils die Spalte cd
in tracks
her, die den Primärschlüssel des entsprechenden
cds
-Eintrags enthält.
Abbildung 1 zeigt das Schema.
Abbildung 1: So liegen die CD-Daten in den Tabellen der Datenbank. Weitere Tabellen können in Zukunft zusätzliche Informationen aufnehmen, zum Beispiel in welchem Keller eine CD gerade lagert. |
Auch mit moderner Weltraumtechnologie wie Class::DBI
muss man die
Datenbank immer noch selbst erzeugen und mit den benötigten
Tabellen initialisieren:
Entweder mit einem Perl-Skript, wie schonmal in [4] gezeigt, oder aber einfach
als Shell-Skript wie in Listing sql.sh
. Damit entstehen die beiden
leeren Tabellen, die der mysql
-Dump in Abbildung 2 illustriert.
01 02 DB=speicher 03 04 mysqladmin --user=root create $DB 05 06 mysql --user=root --database=$DB <<EOT 07 CREATE TABLE cds ( 08 id INT AUTO_INCREMENT, 09 cddbid VARCHAR(10), 10 title VARCHAR(40), 11 artist VARCHAR(40), 12 category VARCHAR(40), 13 PRIMARY KEY (id) 14 ) 15 EOT 16 17 mysql --user=root --database=$DB <<EOT 18 CREATE TABLE tracks ( 19 id INT AUTO_INCREMENT, 20 cd INT, 21 track INT, 22 song VARCHAR(40), 23 PRIMARY KEY (id) 24 ) 25 EOT
Abbildung 2: MySQL hat die Tabellen gespeichert. |
Statt weiter SQL zu sprechen, ziehen wir jetzt aber einfach die
Abstraktionsschnittstelle Class::DBI
ein. Die Datenbank speicher
und
beide Tabellen cds
und tracks
werden durch Klassen repräsentiert:
Wie in Abbildung 3 dargestellt, stammt das die Datenbank speicher
repräsentierende CD::Collection::DBI
direkt von Class::DBI
ab. In Listing CD.pm wird die Klasse in den Zeilen 10 bis 15 definiert.
use base
is neu für die Angabe der Basisklasse, ein eleganter
Ersatz für den etwas Geek-haften @ISA
-Array.
Die Klasse zeigt allerdings nur ein Stück Initialisierungscode, ohne
wirklich Methoden oder Daten zu definieren -- alles wird von
Class::DBI
und seinem Klüngel ererbt. Der Aufruf von set_db
legt die verwendete Datenbank mit Benutzername (root
) und Passwort (leer)
fest.
Abbildung 3: Die Vererbungshierarchie der verwendeten Klassen |
Weiter unten folgen dann die Klassen package
CD::Collection::Slot
und CD::Collection::Track
, die die Tabellen
cds
und tracks
logisch repräsentieren und, wie Abbildung 3 zeigt,
beide von der Datenbank-Abstraktionsklasse CD::Collection::DBI
abgeleitet sind.
Um Class::DBI
Genüge zu tun, definieren auch
sie nur etwas Initialisierungscode,
der die repräsentierten Datenbanktabellen festlegt und deren Spaltennamen
an Class::DBI
weitergibt.
Zeile 26 definiert die Relation einer CD zu ihren Tracks: Ein
cds
-Eintrag ist üblicherweise mit mehreren Einträgen in der
Tabelle tracks
verknüpft. Die has_many
-Methode bereitet die
Klasse CD::Collection::Slot
darauf vor, indem sie Class::DBI
mitteilt, dass die Spaltenwerte für cd
in CD::Collection::Track
auf
den Primärschlüssel von CD::Collection::Slot
(implizit id
, da es die erste Spalte in Zeile 24 war) verweisen.
Die Reihenfolge der Tracks ist über deren Position auf der
CD definiert, Zeile 28 legt dies über den Spaltennamen track
(in
der Tabelle tracks
) fest.
Nach dieser Definition ist
tracks()
anschließend der Name einer neuen
CD::Collection::Slot
-Methode, die
zu einem CD::Collection::Slot
-Objekt alle CD::Collection::Track
-Objekte
liefert. Weiter erzeugt Class::DBI
hinter den Kulissen
eine Methode add_to_tracks()
,
mit der man einfach neue Tracks zu einer CD einfügen kann.
Das war's -- jeder, der CD.pm
einbindet,
kann abstrakt auf die Tabellen zugreifen: Daten einfügen, abfragen,
auffrischen, löschen -- alles ohne eine Zeile SQL.
Passieren Fehler, werfen Class::DBI
-Funktionen in der
Standardeinstellung einfach Exceptions,
die, falls sie nicht abgefangen werden, das Programm beenden. Es ist möglich,
dieses Verhalten zu verändern, aber für das heute vorgestellte einfache
Skript soll's genügen.
01 ########################################### 02 package CD; 03 ########################################### 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 ########################################### 10 package CD::Collection::DBI; 11 ########################################### 12 use base q(Class::DBI); 13 CD::Collection::DBI->set_db('Main', 14 'dbi:mysql:speicher', 15 'root', ''); 16 17 ########################################### 18 package CD::Collection::Slot; 19 ########################################### 20 use base q(CD::Collection::DBI); 21 22 CD::Collection::Slot->table('cds'); 23 CD::Collection::Slot->columns( 24 All => qw(id cddbid title 25 artist category)); 26 CD::Collection::Slot->has_many('tracks', 27 'CD::Collection::Track' => 'cd', 28 { sort => 'track' }); 29 30 ########################################### 31 package CD::Collection::Track; 32 ########################################### 33 use base q(CD::Collection::DBI); 34 35 CD::Collection::Track->table('tracks'); 36 CD::Collection::Track->columns( 37 All => qw(id cd track song) 38 ); 39 40 1;
Als Anwendung zeigt Listing addcd
ein Skript, das die gerade im
Laufwerk sitzende CD ausliest, deren Textdaten über die CDDB-Datenbank
vom Internet holt und sie daraufhin in die Datenbank einspeichert.
Der Teil mit der CD und der CDDB-Datenbank ist trivial: Zeile 10
zieht das praktische CDDB_get
-Modul von
Armin Obersteiner herein und importiert die
Funktion get_cddb
.
Zeile 11 zieht Log::Log4perl im Easy-Modus herein und die Zeilen 13 bis 15 konfigurieren es für die nachfolgenden Ausgaben -- ohne diesen neuen Helfer schreibe ich kein Skript mehr.
Der get_cddb()
-Aufruf in Zeile 17 nimmt eine Hash-Referenz auf einige
Parameterwerte entgegen: CDDB-Server, Port, wo die CD im Rechner steckt,
und ob das Programm die Erkennung interaktiv bestätigen soll: CDDB findet
nämlich manchmal mehrere passende CDs,
mit input => 1
lässt es den Benutzer die Richtige interaktiv
auf der Kommandozeile bestätigen. Leider
tut es das in jedem Fall, auch wenn nur ein Eintrag existiert.
Zurück kommt im Erfolgsfall eine
Liste mit Key/Value-Parametern oder undef
im Fehlerfall.
Dieses nicht-optimale Interface kompensiert das Skript mit einem
Array @cddata
, der die Daten von CDDB_get
aufschnappt.
Ist das erste Element undef
, ging etwas schief
(Verbindung zum Server kaputt, CD nicht lesbar, kein
passender Eintrag gefunden), und
Zeile 26 bricht das Skript mit einer Fehlermeldung ab.
01 #!/usr/bin/perl 02 ########################################### 03 # addcd -- Add a CD to the database 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use CD; 10 use CDDB_get qw(get_cddb); 11 use Log::Log4perl qw(:easy); 12 13 Log::Log4perl->easy_init({ 14 level => $DEBUG, 15 layout => "%m%n"}); 16 17 my @cddat=get_cddb({ 18 CDDB_HOST => "freedb.freedb.org", 19 CDDB_PORT => 8880, 20 CDDB_MODE => "cddb", 21 CD_DEVICE => "/dev/cdrom", 22 input => 1, # interactive 23 }); 24 25 unless ($cddat[0]) { 26 LOGDIE "No cddb entry found"; 27 } 28 29 my %cddat = @cddat; 30 31 if(CD::Collection::Slot->search( 32 artist => $cddat{artist}, 33 title => $cddat{title}, 34 )) { 35 LOGDIE "$cddat{artist}/$cddat{title}" . 36 " already in DB - exiting."; 37 } 38 39 INFO "Adding $cddat{artist}/$cddat{title}"; 40 41 my $cd = CD::Collection::Slot->create( 42 { cddbid => $cddat{id}, 43 artist => $cddat{artist}, 44 title => $cddat{title}, 45 category => $cddat{cat}, 46 } 47 ); 48 49 my $n=1; 50 51 foreach my $song ( @{$cddat{track}} ) { 52 INFO "Adding track $n: $song"; 53 54 $cd->add_to_tracks( 55 { track => $n, 56 song => $song, 57 }); 58 59 $n++; 60 }
Insgesamt liefert CDDB_get Werte für die folgenden Schlüssel in der Rückgabeliste:
Zeile 31 feuert die search
-Methode der die Tabelle cds
abstrahierenden CD::Collection::Slot
-Klasse ab und sucht mit
title => $cddat{title}, artist => $cddat{artist},
nach einer CD in der Datenbank, deren Interpret und Titel dem der aktuell untersuchten CD entspricht. Findet sich ein Eintrag, wurde die CD offensichtlich schon einmal eingescannt und Zeile 35 bricht mit einer Nachricht an den Benutzer ab.
Die create
-Methode von CD::Collection::Slot
in Zeile 41 nimmt eine
Hashreferenz auf Key/Value-Paare des neuen Datenbankeintrags entgegen.
Unter anderem geht der Wert für $cddat{id}
in
der Spalte cddbid
in die Datenbank.
Zeile 51 iteriert über alle in @{$cddat{track}}
enthaltenen Songtitel,
numeriert diese in $n
von 1
an aufsteigend durch und ruft für jeden Track
$cd->add_to_tracks()
. Diese Methode interpretiert Class::DBI
wegen
der vorher in CD.pm
(Zeile 26)
definierten has_many
-Beziehung in schlauer Weise und
linkt jeden neuen Track in tracks
sofort mit dem Primärschlüssel
der entsprechenden CD in cds
.
Die benötigten Module DBI
, DBD::mysql
, Class::DBI
und
auch CDDB_get
gibt's, wie immer, auf dem CPAN und lassen sich leicht
mittels einer CPAN-Shell installieren.
Dann schnell eine CD ins Laufwerk gelegt und addcd
aufgerufen:
$ addcd This CD could be: 1: Haindling / Karussell 0: none of the above Choose: 1 Adding Haindling/Karussell Adding track 1: Karussell ... Adding track 14: Bellaria - Solange wir leben
Fertig! Um herauszufinden, was schon alles in der Datenbank steht, eignet sich
ein simples Skript wie das in Listing dumpdb
. Es hangelt sich durch
die CD-Tabelle CD::Collection::Slot
, findet mit tracks()
die
dazugehörigen Einträge in CD::Collection::Tracks
, und gibt alles
auf STDOUT aus:
Haindling: Karussell 1 Karussell ... 14 Bellaria - Solange wir leben Die Toten Hosen: Auswaertsspiel 1 Du Lebst Nur Einmal (Vorher) ... 18 Kein Alkohol (Ist Auch Keine Loesung) ...
01 #!/usr/bin/perl 02 ########################################### 03 # dumpdb -- Dump DB 'speicher' content 04 # Mike Schilli, 2002 (m@perlmeister.com) 05 ########################################### 06 use warnings; 07 use strict; 08 09 use CD; 10 11 for my $cd ( 12 CD::Collection::Slot->retrieve_all()) { 13 14 print $cd->artist(), ": ", 15 $cd->title(), "\n"; 16 17 for my $track ($cd->tracks()) { 18 printf " %2d: %s\n", 19 $track->track(), 20 $track->song(); 21 } 22 }
Das war's bereit's -- einfach und übersichtlich, nicht? Ich sage eine
große Zukunft für Class::DBI
voraus. Bis zum nächsten Mal!
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. |