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