mhripper

Was ist 'mhripper'?

'mhripper' ist ein Perl-Script, das als Frontend für 'cdda2wav' und 'lame' dient. 'mhripper' macht folgendes:

Das Script ist von dieser Seite herunterzuladen: Download

Abhängigkeiten

'mhripper' ist lediglich ein Frontend, das heißt, daß die eigentliche Arbeit von anderen Programmen gemacht wird. Die benötigten Programme sind:

Zusätzlich zu den o.a. Programmen werden noch folgende Perl-Module benötigt (beide über CPAN zu finden):

Installation

Nichts einfacher als das (als 'root'):

 #> cp mhripper.pl /usr/local/bin/mhripper
 #> chmod +x /usr/local/bin/mhripper
    

'mhripper' benutzen

Nach der Installation läßt sich 'mhripper' einfach an der Konsole aufrufen. Wenn keine Optionen angegeben werden, werden folgende Voreinstellungen benutzt:

  1. CD-Rom-Laufwerk: /dev/cdrom
  2. Ziel-Verzeichnis: /images
  3. Auszulesende Tracks: alle

Um die o.a. Voreinstellungen (zu 1. und 2.) zu ändern, kann das entweder im Perl-Code von 'mhripper.pl' selbst (für eigene Standardwerte), oder aber auch per Kommandozeilen-Optionen geschehen.

Ändern der Standardwerte im Perl-Code

Dazu müssen folgende Zeilen am Anfang von 'mhripper.pl' geändert werden:

 my $device     = "/dev/cdrom";
 my $target_dir = "/images";
    

Kommandozeilen-Optionen

'mhripper' kann mit folgenden Optionen gestartet werden:

--help
zeigt die Kommandozeilen-Optionen an
--dev
hiermit kann das CD-Rom-Laufwerk geändert werden
Beispiel:
mhripper --dev=/dev/cdrecorder
--dir
hiermit kann das Ziel-Verzeichnis geändert werden
Beispiel:
mhripper --dir=/home/martin/mp3
--tracks
hiermit kann eine Auswahl der auszulesenden CD-Tracks getroffen werden. Die Liste ist eine durch Kommata getrennte folge der CD-Tracks.
Beispiele:
mhripper --tracks=1,5,7 (liest den 1., 5. und 7. Titel der CD aus)
mhripper --tracks=10 (liest nur Titel Nr. 10 der CD aus)

Ergebnisse

Nach dem erfolgreichen Durchlauf von 'mhripper' liegen die .mp3-Dateien und die Playlist (.m3u) im folgenden Pfad vor:

 <DIR>/<KÜNSTLER>-<CD-TITEL>/<TRACK_NR>_<TRACK_NAME>.mp3
   ^   ^^^^^^^^^^^^^^^^^^^^^/^^^^^^^^^^^^^^^^^^^^^^^.mp3
   |             |                       |
   |    aus CDDB-Abfrage    /     aus CDDB-Abfrage
   |
 entweder das Standard-Verzeichnis oder
 das über --dev=<DIR> definierte.
    

Die .mp3-Dateien werden standardmäßig mit der vorangestellten Track-Nr. gespeichert, weil ich mich ständig darüber geärgert habe, daß mein MP3-fähiger DVD-Player an der Stereo-Anlage keine Playlisten einlesen kann und deshalb alphabetisch vorgeht. Mit der vorangestellten Track-Nr. spielt er somit auch ohne Playlist in der richtigen Reihenfolge ab.

Die Playlist liegt ebenfalls in dem o.a. Pfad und heißt:

 <KÜNSTLER>-<CD-TITEL>.m3u
    

Besonderheit der Playlist

Die von 'mhripper' erstellte Playlist ist 'kummulativ'. Das bedeutet, daß sie bei Bedarf ergänzt wird.

Beispiel:
Ich lese die Tracks 3 und 5 einer CD aus. Später fällt mir ein, daß es vielleicht ganz hübsch wäre, wenn ich Track 10 auch noch als .mp3 vorliegen hätte. In diesem Fall wird die Playlist um Track 10 erweitert.

Voraussetzung ist aber, daß keine .mp3-Dateien gelöscht wurden.

Was macht der Code von 'mhripper' genau?

Diejenigen, die (meinen) Perl-Code 'lesen' können und ohnehin erst die Sourcen kontrollieren, bevor sie einen Code ausführen, brauchen hier nicht weiterlesen. Alle anderen müssen mir jetzt vertrauen ;-)

Script Teil - 1
Startup

 #!/usr/bin/perl
 use strict;
 use diagnostics;
 use Getopt::Long;
 use Term::ReadKey;
    

Zuerst wird der Pfad zu Perl (/usr/bin/perl) angegeben, damit die Datei ohne "perl"-Aufruf gestartet werden kann. Danach werden vier Perl-Module deklariert, die ich für das Script benötige ('strict' und 'diagnostics' helfen bei der Fehlersuche; 'Getopt::Long' ist für das Abfangen der Kommandozeilen-Optionen erforderlich; 'Term::ReadKey' brauche ich nur für eine hübschere Ausgabe des Scripts)

Script Teil - 2
Benutzer-Variablen

 #######################################################
 # default-values ...                                  #
 # feel free to change this to your needs              #
 #######################################################
 my $device     = "/dev/cdrom"; # set default cd-reader
 my $target_dir = "/images";    # set default output-path
    

An dieser Stelle werden die zwei wichtigsten Vorgabe-Werte deklariert. Diese können geändert werden, um sie an die eigenen Standardeinstellungen anzupassen. ('$device' = CD-Rom-Laufwerk; '$target_dir' = Ausgabeverzeichnis)

Script Teil - 3
Kommandozeilen-Optionen

 #######################################################
 #            DONT CHANGE ANYTHING BELOW!              #
 #        unless you know what you're doing ;-)        #
 #######################################################
 my $tracks     = 0; # '0' means 'all'
 my $needhelp   = 0; # '0' means 'don't show usage-message by default'

 if ( ! &GetOptions(
		   "tracks=s" => \$tracks,
		   "dev=s" => \$device,
		   "dir=s" => \$target_dir,
		   "help" => \$needhelp
		  )
   ) {exit 1;}

 if ($needhelp == 1) { &printusage; }

 chomp $tracks;
 chomp $device;
 chomp $target_dir;

 my $temp_dir   = "$target_dir\/tmp";
 if (not -e $temp_dir) { system ("mkdir -p $temp_dir"); }
    

Dieser Teil wertet die Kommandozeilen-Optionen (z.B.: --dev=<DEVICE>) aus. Zunächst werden die Standardwerte für die Variablen '$tracks' und '$needhelp' gesetzt. Diese Werte werden benutzt, wenn für die beiden Variablen keine Kommandozeilen-Option angegeben wird. ('$tracks = 0' bedeutet 'alle Titel der CD auslesen'; '$needhelp = 0' bedeutet 'Optionen nicht standardmäßig anzeigen sondern nur bei ausdrücklichem '--help')

Der 'GetOptions'-Block liest nun die Optionen von der Kommandozeile und weist den entsprechenden Variablen ('$tracks', '$device', '$target_dir') den angegebenen Wert zu. Wenn die keine Optionen angegeben werden, werden die vorher für die Variablen definierten Standardwerte benutzt.

Wenn die Option '--help' an der Kommandozeile angegeben wurde, wird der Wert der Variablen '$needhelp' auf '1' gesetzt. Hat '$needhelp' den Wert '1' wird das Unterprogramm 'printusage' aufgerufen (s.u.). Diese Auswertung erfolgt im zweiten 'if'-Block.

Die drei 'chomp'-Aufrufe entfernen einen evtl. vorhandenen CR/LF (Carriage Return / Line Feed) von den Variablen, da die Variable durchaus auch am Kommandozeilennende angegeben worden sein kann.

Zu guter Letzt (für diesen Abschnitt) wird das Verzeichnis für die temporären .wav-Dateien erstellt, falls es noch nicht existiert. Das Verzeichnis liegt innerhalb von '$target_dir' und heißt schlicht 'tmp'.

Script Teil - 4
CDDB-Abfrage - Album-Informationen

 my ($track_nr, $title, $playlist);
 my @rip_tracks;

 system ("cda -dev $device on >/dev/null 2>&1");

 my ($width)    = GetTerminalSize(1);
 &header;
 print " Extracting Tracks\n";
 print " from: $device\n";
 print " to:   $target_dir\n";
 &drawline;

 my $id  = `cda -dev $device extinfo | grep 'disc ID'`;
 my $artist = `cda -dev $device extinfo | grep 'Artist:'`;
 my $album  = `cda -dev $device extinfo | grep 'Title:'`;

 if ($id =~ m/disc ID: (.{1,})\Z/i)
  {
    $id = $1;
    chomp $id;
  }

 if ($artist =~ m/\AArtist: (.{1,})\Z/i)
  {
    $artist = $1;
    chomp $artist;
  }

 if ($album =~ m/\ATitle: (.{1,})\Z/i)
  {
    $album = $1;
    chomp $album;
  }
    

Zuerst werden drei globale Variablen und ein Array initialisiert, die später auch dem Unterprogramm 'extract_tracks' zur Verfügung stehen sollen.

Als nächstes wird das CD-Laufwerk für 'cda' nutzbar gemacht. Da die Ausgabe langweilig und unschön ist, wird sie direkt nach '/dev/null' geschickt - außer Fehlermeldungen ... die werden durch '2>&1' weiterhin am Bildschirm angezeigt.

Um die Bildschirmausgabe etwas gefälliger zu gestalten, wird mit 'GetTerminalSize()' die aktuelle Breite des Terminals in Zeichen abgefragt. Dieser Wert wird an die Variable '$width' übergeben, die im Unterprogramm 'drawline' benötigt wird (und nur da).

Das Unterprogramm 'header' erzeugt einen hübschen Kopf für das Programm. Danach wird der Benutzer darüber informiert, daß etwas passiert - ist aber eigentlich nur Spielerei ;-).

Der eigentliche Teil der CDDB-Abfrage kommt jetzt. Die Variablen '$id', '$title', '$album' werden direkt mit den Werten der 'cda/grep'-Kombination gefüllt. Damit die Variablen aber auch wirklich nur die gewünschten Werte enthalten, müssen sie noch etwas verfeinert ausgelesen werden, was durch die drei 'if'-Blöcke darunter geschieht.

Script Teil - 5
CDDB-Abfrage - Titel-Informationen

 if ($tracks ne 0)
  {
    if ($tracks =~ m/,/i)
      {
	@rip_tracks = split (/,/, $tracks);
      }
    else
      {
	push (@rip_tracks, $tracks);
      }

    foreach (@rip_tracks)
      {
	chomp $_;

	if (length $_ < 2) {
	  $track_nr = "0$_";
	} else {
	  $track_nr = $_;
	}

	$title  = `cda -dev $device toc | grep ' $track_nr '`;
	if ($title =~ m/ (\d\d) \d\d\:\d\d  (.{1,})\Z/i)
	  {
	    $title = $2;
	    chomp $title;
	  }
	&extract_tracks;
      }
  }
 elsif ($tracks == 0)
  {

    my @toc = `cda -dev $device toc`;
    foreach (@toc)
      {
	if ($_ =~ m/ (\d\d) \d\d\:\d\d  (.{1,})\Z/i)
	  {
	    $track_nr = $1;
	    $title = $2;
	    chomp $title;
	    &extract_tracks;
	  }
      }
  }
 system ("cda -dev $device off >/dev/null 2>&1");
    

Die Album-Informationen wurden für die gesamte CD eingelesen, da sie ohnehin auf jeden Track der CD zutreffen (s. Script Teil - 4). Bei den Titeln ist das aber anders. Die CDDB-Info hierfür wird einzeln eingelesen, da jeder Track ja einen anderen Namen hat.

Hier sind zwei ähnliche 'if/elsif'-Blöcke zu sehen. Warum zwei? Nun der erste ist dafür da einzelne Tracks auszulesen, die mit z.B. '--tracks=1,4,6' angegeben wurden. Der zweite ('elsif') Block ist dafür da, alle Track-Informationen auf einmal zu holen, falls nämlich keine '--tracks'-Option angegeben wurde und somit die gesamte CD eingelesen werden soll.

if ($tracks ne 0)

Wenn also die Variable '$tracks' ungleich '0' ist (also die Option '--tracks' verwendet wurde), wird sie zuerst überprüft, ob sie Kommata enthält (dann sollen mehrere Titel ausgelesen werden), oder nicht (dann handelt es sich nur um einen Titel). Die herausgefundene/n Titelnummer/n wird dann in das Array '@rip_tracks' geschrieben.

Im nächsten Schritt wird für jede Titelnummer aus '@rip_tracks' überprüft, ob es sich um eine zweistellige Zahl (z.B. '12') handelt. Wenn die Titelnummer hingegen einstellig ist (z.B. '2') wird eine führende Null vorangestellt. So wird aus '2' dann '02'. Dieses ist für den nächsten Schritt nötig, in dem wieder mittels 'cda' eine CDDB-Abfrage durchgeführt wird. Die Abfrage gibt eine komplette Titelliste der CD (toc = table of contents) zurück, aus der mittels 'grep' der korrekte Titel (der in der Liste mit zweistelligen Tracknummern steht) ausgelesen wird.

Aus der Liste wird mit 'grep' aber nicht nur der Titel ausgelesen, sondern die gesamte Zeile, auf die die Tracknummer zutrifft. Um nur den Titel zu erhalten wird die Suche wieder etwas verfeinert ($title =~ m/...).

Nachdem jetzt alle benötigten Variablen zusammengesucht sind, wird an das Unterprogramm 'extract_tracks' übergeben, daß die Tracks dann letztendlich verarbeitet.

elsif ($tracks == 0)

Wenn also alle Tracks der CD eingelesen werden sollen, kann man die Tracks etwas leichter auslesen. Dazu wird wieder per 'cda' die 'toc' aus der CDDB abgefragt. Da die Titelnummern in der 'toc' ohnehin zweistellig sind, braucht hier weder 'gegrept' noch 'zweistellig gemacht' werden. Es reicht ein einfaches Pattern-Matching ($_ =~ m/...) und die Übergabe an 'extract_tracks'.

Script Teil - 6
Die 'kummulative' Playlist

 #
 # beautify the playlist
 #
 print "\n";
 &drawline;
 print "writing cumulative playlist to:\n";
 print " $playlist\n";
 print "\n\.\.\. all done!\n\n";

 my @pl = `less $playlist`;
 @pl = sort(@pl);

 my @new_pl;
 my $entry = "";

 foreach (@pl)
  {
    if ($entry ne $_)
      {
	push (@new_pl, $_);
	$entry = $_;
      }
  }

 open (NEWPL, ">$playlist");
 print NEWPL ("@new_pl");
 close (NEWPL);
    

Die ersten fünf Zeilen erzeugen lediglich eine Ausgabe für den Benutzer. Danach wird die Playlist in das Array '@pl' eingelesen. Die Variable '$playlist' wird erst in 'extract_tracks' mit ihrem korrekten Wert gefüllt. Da 'extract_tracks' aber immer vor diesem Abschnitt ausgeführt wird, ist das zwar unelegant aber möglich.

Nun wird die Playlist alphabetisch sortiert. Da die Tracknamen die führende Tracknummer enthalten wird so die korrekte Reihenfolge erzeugt.

Jetzt wird ein zweites Array namens '@new_pl' erzeugt, und eine leere Variable '$entry' deklariert.

Was nun folgt läßt sich sicher eleganter lösen, aber es funktioniert zumindest. Das Array '@pl' wird nun zeilenweise durchlaufen. Jedesmal, wenn '$entry' nicht dem derzeitig aktuellen Eintrag ('$_') in '@pl' entspricht wird '$_' in das Array '@new_pl' geschrieben. Sinnfrei? Nein! Das beschriebene Szenario in einem praktischen Beispiel:

Das Array '@pl' kann aufgrund des Unterprogramms 'extract_tracks' doppelte Einträge enthalten, wenn dieselbe CD mehrfach gerippt wird. Also müssen die doppelten Einträge gefunden und gelöscht werden. Da löschen aber aufwändiger ist als neu schreiben, gibt es das zweite Array '@new_pl', in dem dann eine neue Playlist erzeugt wird. Nun aber wirklich zum Beispiel ;-):

'@pl' enthält folgende Werte:

 01_Track.mp3
 03_Track.mp3
 03_Track.mp3
 13_Track.mp3
 13_Track.mp3
    

Nun macht das Script schrittweise folgendes:

zu bearbeitende
Zeile von
'@pl'
Wert von
'$entry'
Wert von
'$_'
'$entry'
ungleich
'$_'
Eintrag in
'@new_pl'
'$entry'
wird auf
'$_' gesetzt
01_Track.mp3 - leer - 01_Track.mp3 ja 01_Track.mp3 01_Track.mp3
02_Track.mp3 01_Track.mp3 02_Track.mp3 ja 02_Track.mp3 02_Track.mp3
02_Track.mp3 02_Track.mp3 02_Track.mp3 nein - nichts - 02_Track.mp3
13_Track.mp3 02_Track.mp3 13_Track.mp3 ja 13_Track.mp3 13_Track.mp3
13_Track.mp3 13_Track.mp3 13_Track.mp3 nein - nichts - 13_Track.mp3

Das Ergebnis ist dann die Playlist, die in der markierten Spalte ('@new_pl') zu sehen ist. Diese Playlist wird nun wiederum als neue Playlist in das Verzeichnis geschrieben. - Voila`.

Unterprogramm 'drawline'

 sub drawline {
   for (my $i = 1; $i < ($width +1); $i++)
     {
       print "-";
     }
   print "\n";
 }
    

Dieses Unterprogramm erzeugt eine Linie aus Bindestrichen ('-') für die gesamte Bildschirmbreite - nichts Spektakuläres. Für diese Spielerei ist übrigens das Perl-Modul 'Term::ReadKey' notwendig.

Unterprogramm 'header'

 sub header {
   &drawline;
   print "         \| Rip and Encode Audio-CDs using CDDA2WAV and LAME\n";
   print "mhripper \|   (c) 2002, Martin Holz \\n";
   print "         \|           http:\\\\www\.perl-newbie\.de\n";
   &drawline;
 }
    

Auch hier nichts spektakuläres ... nur die Ausgabe eines Headers für den Programmstart und die Hilfe

Unterprogramm 'printusage'

 sub printusage {
   &header;
   print "Usage:\n";
   print "------\n";
   print "mhripper [--help] [--dev] [--dir] [--tracks]\n\n";
   print "  --help   = displays this message\n\n";
   print "  --device = set a specific CDROM-device (default: /dev/cdrom)\n";
   print "             Example: mhripper --device=/dev/cdrecorder\n\n";
   print "  --dir    = set OUTPUT-directory (default: /images)\n";
   print "             Example: mhripper --dir=/home/martin/mp3\n\n";
   print "  --tracks = comma seperated list to select tracks for\n";
   print "             ripping and encoding\n";
   print "             Examples: mhripper --tracks=1\n";
   print "                       mhripper --tracks=1,4,8,11\n\n";
   &drawline;
   { exit }
 }
    

Schon wieder nichts aufregendes ;-) Dieses Unterprogramm erzeugt lediglich die kurze Hilfe für die Option '--help'.

Unterprogramm 'extract_tracks'

 sub extract_tracks {
   my $rip_nr;
   if ($track_nr =~ m/0(\d)/i) {
     $rip_nr = $1;
   } else {
     $rip_nr = $track_nr;
   }

   my $rip_name = $title;
   my $id3_artist = $artist;
   my $id3_album = $album; 

   $rip_name =~ s/ /_/g; #
   $artist   =~ s/ /_/g; # subset whitespaces with underlines
   $album    =~ s/ /_/g; #

   my $album_dir = "$target_dir\/$artist-$album";
   $playlist  = "$album_dir\/$artist-$album\.m3u"; 

   # beautify output ;-)
   &drawline;
   write;
   &drawline;

   if (not -e $album_dir) { system ("mkdir -p $album_dir"); }
   system ("touch $playlist");

   print "\* Step 1: ripping track \[using CDDA2WAV\]";
   system ("cdda2wav -Q -D $device -I cooked_ioctl -q -H -t $rip_nr $temp_dir\/\"$track_nr\_$rip_name\"");
   print "\.\.\. done\n";

   print "\* Step 2: encoding track \[using LAME\]\n";
   system ("lame -b 192 --tt \"$title\" --ta \"$id3_artist\" --tl \"$id3_album\" 
$temp_dir\/\"$track_nr\_$rip_name\.wav\" $album_dir\/\"$track_nr\_$rip_name\.mp3\"");

   system ("echo \"$track_nr\_$rip_name\.mp3\" >> $playlist");
   unlink ("$temp_dir\/$track_nr\_$rip_name\.wav");

   return ($playlist);
 }

 format STDOUT =
  Track: @< 
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
         $track_nr, $title
 Artist: 
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
         $artist
  Album: 
@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<
         $album
 .
    

Endlich mal wieder ein paar richtige Funktionen ;-). Also dieses Unterprogramm ist eigentlich das Herzstück des gesamten Programms. Hier werden nämlich jetzt die ausgewählten (oder alle) Tracks der CD ausgelesen, als .wav gespeichert und dann in .mp3 umgewandelt. Zu guter Letzt werden dann noch die Playlist geschrieben und die temporären Dateien wieder gelöscht. - Aber der Reihe nach:

Zuerst wird die Tracknummer wieder um die führende Null erleichtert. Dann werden ein paar Variablen deklariert, die für das Speichern gebraucht werden ('rip_nr', 'rip_name', 'id3_artist' und 'id3_album'). Diesen Variablen wird dann der Wert der bereits bekannten CDDB-Werte zugewiesen. Anschließend werden in einigen Variablen die Leerzeichen durch Unterstriche ersetzt. Hintergrund dafür ist, daß die ID3-Tags der .mp3-Dateien z.B. einen Titel mit Leerzeichen (Whitespaces), die Dateinamen selbst aber statt mit Whitespaces mit Unterstrichen gespeichert werden sollen. Aus diesen Variablen werden dann die Verzeichnisnamen und Tracknamen sowie die ID3-Tags der .mp3-Dateien erstellt. Die Umformung von Whitespaces auf Unterstriche ist zwar rein kosmetischer Natur, aber durchaus empfehlenswert.
Beispiele:

 ID3-Tag Album  : Mechanical Animals
 Verzeichnisname: Mechanical_Animals

 ID3-Tag Title  : Great Big White World
 Dateiname      : Great_Big_White_World

 Verzeichnisname mit Whitespaces:   /images/Marilyn Manson-Mechanical Animals/
 Verzeichnisname mit Unterstrichen: /images/Marilyn_Manson-Mechanical_Animals/
    

Die Dateien mit Unterstrichen lassen sich später auf der Konsole und je nach Betriebssystem etwas leichter handhaben als die mit Whitespaces.

So, nun zu einer unscheinbaren Stelle ... 'write;'. Was wird denn hier geschrieben? Alles, was am Dateiende unter 'format STDOUT =' an Variablen geschrieben wird. Dies erzeugt dann eine Tabellenartige Information welcher Track gerade bearbeitet wird.

Anschließend folgt dann ein Test, ob das Ausgabeverzeichnis bereits existiert (Ausgabeverzeichnis = <DIR>/<KÜNSTLER>-<ALBUM>). Wenn das nicht der Fall ist, wird es erstellt.

Über den 'touch'-Befehl wird auch gleich eine (leere) Datei mit dem in '$playlist' definierten Namen (<KÜNSTLER>-<ALBUM>.m3u)

Nun folgt 'Step 1' nämlich das Auslesen der Datei von CD. Dazu wird 'cdda2wav' bemüht. Der Aufruf von 'cdda2wav' ist relativ lang. Eine Erläuterung der Funktionen ist hier zu finden. Als Ergebnis dieses Aufrufs liegt dann eine .wav Datei im temporären Verzeichnis vor.

Direkt danach kommt 'Step 2' - das encoden der eben erzeugten .wav-Datei in eine .mp3-Datei mittels 'lame'. Der Aufruf von 'lame' ist ähnlich lang. die Optionen für 'lame' sind hier zu finden.

Nun wird nur noch der Name der soeben erzeugten .mp3-Datei an die Playlist angehängt und mittels 'unlink' die .wav-Datei gelöscht.

Download

Das komplette Script (wie oben beschrieben) zum Download: mhripper.pl
Eine verbesserte Variante steht unter mhripper.zip zur Verfügung.


Martin Holz | Impressum

Valid HTML 4.01!