Vergesslicher Archivar (Linux-Magazin, August 2006)

Der von onlinetvrecorder.com angebotene Service klingt fast zu gut, um wahr zu sein. Dort kann man kostenlos beliebige Sendungen des deutschen Fernsehprogramms aufzeichnen und herunterladen. Zwar kränkelt die Website hin und wieder beim Aufzeichnen und der Download geht nur tief in der Nacht einigermaßen zügig. Aber dafür, im Ausland deutsches Fernsehen zu genießen, nimmt man ja so einiges in Kauf.

Haben sich so im Laufe einiger Wochen einige Dutzend Sendungen auf der Festplatte angesammelt, stellt sich die Frage nach einer Verwaltungssoftware, die den User aus den verfügbaren Sendungen auswählen laesst und die alte Schinken, die eine zeitlang nicht angerührt wurden, von der Platte tilgt.

Hier drängt sich die Analogie zu einem ``Tivo'' auf. Diese von der Firma ``Tivo'' vor einigen Jahren auf den Markt gebrachten digitalen Fernsehrekorder und ihre Klone sind auf dem amerikanischen Markt praktisch nicht mehr wegzudenken. Jedes Kind kennt den Markennamen. Die Geräte bieten eine einfach zu bedienende Oberfläche, die den Nutzer Fernsehsendungen aufzeichnen lassen und diese dann auf einer Festplatte zum späteren Sehgenuss aufbewahren. So lassen sich nicht nur lästige Werbeblocks im Schnelldurchlauf durchqueren, sondern auch das sogenannte ``Time-Shifting'' realisieren: Mit massenweise eingelagerten Sendungen sieht man nicht mehr dann fern, wenn eine Sendung ausgestrahlt wird, sondern wenn man Zeit hat zum Fernsehen.

Wegwerfen kann man's immer noch

Abbildung 1 zeigt eine kleine Auswahl aufgezeichneter Programme auf meinem 5 Jahre alten (und natürlich aufgebohrten) Tivo. Das Gerät nimmt ständig automatisch neue Sendungen auf, aber der Plattenplatz ist natürlich begrenzt. Deswegen löscht der Tivo alte Programme nach einigen Tagen selbständig, es sei denn, der Benutzer hat sie eigenhändig als ``Save until I delete'' markiert. Der Tivo unterscheidet zwischen Aufnahmen, die kurz vor der Löschung stehen (Ausrufezeichen), noch mehrere Tage (keine Markierung) oder einen Tag (gelber Punkt) Gnadenfrist haben oder aber unbegrenzt aufbewahrt werden (grüner Punkt).

Abbildung 1: Auswahl aufgezeichneter Fernsehprogramme auf dem digitalen Videorekorder "Tivo"

Das heute vorgestellte Skript tv bildet eine einfache Version dieser Benutzerschnittstelle nach. Statt den bekannten graphischen Toolkits wie perl/Tk, Gtk oder wxWindows nutzt es die auf der Curses-Bibliothek basierende Widget-Sammlung Curses::UI. Mit ihr lassen sich einfach typische GUI-Elemente wie Dialoge, Menüs oder Listboxen in einem ASCII-Terminal programmieren. Der Look von 1980 ist wieder da, wir schwelgen in Nostalgie!

Die Videodateien erwartet das Skript in einem voreingestellten Verzeichnis, das es alle 60 Sekunden durchforstet. Stellt es Änderungen fest, frischt es die Oberfläche entsprechend auf. Auch prüft es laufend, ob die Gesamtheit aller Videodateien eine maximal zulässige Größe überschritten haben, voreingestellt sind 20 Gigabytes. Ist dies der Fall, löscht es die ältesten Dateien, die nicht speziell markiert wurden, ohne Nachzufragen von der Platte, solange, bis die Höchstmarke wieder unterschritten wird.

Abbildung 2: Ähnliche Oberfläche, implementiert mit Curses::UI.

Die Navigation in der von tv dargestellten Listbox erfolgt entweder mit den Cursortasten (samt Page-Up/Down) oder mit den vi-Nutzern bekannten Tasten 'k' (nach oben) und 'j' (nach unten). Zum manuellen Löschen einer Datei drückt der User die Taste ``d'' (delete). Erhält der anschließend gezeigte Bestätigungsdialog (Abbildung 3) ein ``y'' oder wird die Return-Taste gedrückt, während der Cursor über dem ``OK'' steht, löscht tv die Datei von der Platte und frischt die Listbox auf. Um eine Datei mit einem Stern zu markieren, sie also vor der automatischen Löschung durch den minütlich erscheinenden Plattenplatzkontrolleur zu schützen, drückt der User die Taste ``*'' auf einem angewählten Listboxeintrag. Um die Sendung mit dem mplayer abzuspielen, genügt es, die Return-Taste auf einer ausgewählten Datei zu drücken. Der unter [3] erhältiche Tausendsassa spielt alle gängigen Videoformate ab. Ein Druck auf die Taste ``q'' beendet einen einmal gestarteten Mplayer wieder, der sich auch mittels Tastaturkommandos vor- und zurückspulen lässt. Um das Programm tv zu beenden, genügt ebenfalls ein Druck auf die Taste ``q''.

Abbildung 3: Dialog zum Bestätigen einer Löschung nach dem Drücken der Taste "d".

Zunächst zieht Listing tv die Modul Curses::UI::POE und Curses herein, die beide auf dem CPAN erhältlich sind. Die praktischen Curses-Widgets sind in dem Module Curses::UI enthalten, aber damit das Skript auch multitasking-fähig ist (um zum Beispiel die periodischen Auffrischungsarbeiten durchzuführen), definiert Curses::UI::POE eine abgeleitete Klasse, die das GUI in die Eventschleife des POE-Frameworks einbindet. POE kam im Snapshot schon des öfteren zum Einsatz, meist um graphische GUIs mittels kooperativem Multitasking ruckelfrei laufen zu lassen, obwohl das steuernde Programm zeitaufreibene Nebentätigkeiten ausführen muss.

Aufwachen im Minutentakt

Der in Zeile 11 aufgerufene Konstruktor legt mit der Option color_support fest, dass die neue Terminal-GUI Farben unterstützt. Der Parameter inline_states definiert den Startzustand _start, den der POE-Kernel kurz nach dem Anlaufen automatisch anspringt. Dort sorgt die Methode delay() dafür, dass nach exakt 60 Sekunden der Zustand wake_up aufgerufen wird, der die ab Zeile 90 definierte Funktion wake_up_handler ablaufen lässt. Dort untersucht die Methode rescan() des später behandelten Moduls Videodirs das Videoverzeichnis und notiert sich die Namen aller dort vorhandenen Dateien und deren Datumsstempel. Weiter befindet sich dort eine kleine Datenbank, in der steht, wie lange der Benutzer die einzelnen Filme behalten möchte. Dies alles liest Videodirs::rescan() ein und speichert es in einer internen Datenstruktur, die die anschließend aufgerufene Funktion redraw() einliest und die Listbox der GUI aktualisiert.

Die in Zeile 95 aufgerufene Methode Videodirs::shrink() lässt das Videoverzeichnis durch das Löschen alter Filme schrumpfen, falls deren Größe einen voreingestellten Wert überschreitet. Dem wake_up_handler bleibt anschließend nur noch, dem POE-Kernel über die Methode delay() mitzuteilen, dass dieser ihn bitte in 60 Sekunden wieder zurückrufen möge, bevor die Funktion endet und die Kontrolle wieder an den POE-Kernel übergeht. Dieser widmet sich dann dem Abarbeiten der Benutzereingaben und dem permanentent Auffrischen des GUI.

Boxen aus Buchstaben

Ab Zeile 20 baut tv die ASCII-GUI auf. Die Methode add() fügt zunächst ein neues Widget vom Typ Window ein, das den gesamten verfügbaren Platz des gerade laufenden Terminals beansprucht. Anschließend erhält das Window-Objekt seinerseits drei Widgets, die mit add() der Reihe nach von oben nach unten in die GUI eingelassen werden: Den oberen Info-Balken $TOP, die Listbox $LBOX und den unteren Info-Balken $BOTTOM (Abb. 1).

Die ersten beiden Parameter, die add() entgegennimmt, sind ein Kürzel für das so erzeugte neue Widget und dessen Typ. Die beiden Balken sind vom Typ Curses::UI::Label, der Code für die Listbox mit den Videoeinträgen ist in Curses::UI::Listbox definiert. Die Option -y der add()-Methode legt die vertikale Position der Balken fest (1: oberste Reihe, -1: unterste Reihe, -bg (Background) bestimmt die Hintergrundfarbe und -fg (Foreground) die Farbe der Beschriftung. -width -1 breitet die Infobalken über das gesamte Terminal aus. -paddingspaces erweitert die blauen Balken bis ans Zeilenende, auch wenn der Beschriftungstext kürzer ist. Die Parameter werden normalerweise als Paare im Format key => value überreicht. Nur wegen der Platzbeschränkung im Linux-Magazin kam die platzsparende Schreibweise mit qw(...) zum Einsatz, die die Optionen im String an den Wortgrenzen voneinander trennt und als Liste weitergibt.

Die Listbox lässt mit -padtop 1 und -padbottom 1 oben und unten Platz für die beiden Balken, anstatt sich rücksichtslos auszubreiten. Mit -border 1 wird die Listbox von einem ansprechenden dünnen blauen Rahmen umgarnt. Zwei verschiedene Listbox-Events werden vom Skript verarbeitet. -onselchange kommt zum Zug, fallls der User eine Cursortaste betätigt und so den Listbox-Cursor um eins weiterschiebt. In diesem Fall wird die ab Zeile 57 definierte Funktion changed aufgerufen, die die Metadaten des markierten Videofiles in der Fußzeile der GUI ausgibt. Dort steht dann, das wievielte Element von wievielen Videofiles insgesamt ausgewählt wurde (z.B. 1/74), wie alt die Datei ist, wieviel Speicherplatz sie verbraucht und wie lange sie noch zu leben hat, falls der User nichts unternimmt. ``TTL 4.3'' bedeutet zum Beispiel, dass die 'time to live' 4.3 Tage beträgt. Nach diesem Zeitpunkt ist die Datei vogelfrei, das heißt, sie wird ohne Nachfrage gelöscht, falls dies aufgrund einer knappen Speicherplatzsituation erforderlich ist.

Welcher Eintrag in der Listbox gerade selektiert ist, findet die Methode get_active_id() des Listbox-Objekts $LBOX heraus, die den Index des entsprechenden Listelements zurückgibt. Das später besprochene Modul Videodirs.pm hält sich eine Datenstruktur, die allen Einträgen, die in der Listbox stehen, über die ID des Listbox-Eintrags die Metadaten der zugehörigen Videodatei zuordnet.

Der zweite von der Listbox verarbeitete Event ist -onchange. Dieser wird ausgelöst, falls der User auf einem selektierten Eintrag die Enter-Taste betätigt oder mit der Maus einen Eintrag anklickt. Dies ist für tv das Signal, dass der User das ausgewählte Video ansehen möchte. Zeile 68 ruft den mplayer mittels Backticks im Hintergrund auf. Dies ist wichtig, denn die GUI muss weiter Tastatureingaben verarbeiten, sonst ``friert'' sie ein.

Zusätzlich zu dieser Callback-Definition der Listbox legt Zeile 39 fest, dass die Funktion selected() aufrufen wird, falls der User die Return-Taste drückt. Das Makro KEY_ENTER() ist im Modul Curses definiert und bezeichnet die Return- oder Enter-Taste. Dank des vorher definierten OnChange-Eventhandlers der Listbox geschähe dies zwar auch ohne die explizit festgelegte set_binding-Anweisung (denn einen Listbox-Eintrag auszuwählen, löst einen OnChange-Event aus), allerdings funktioniert das nicht, falls der User denselben Eintrag nochmals auswählt!

Listing 1: tv

    001 #!/usr/bin/perl -w
    002 use strict;
    003 use Videodir;
    004 use Curses::UI::POE;
    005 use Curses;
    006 
    007 my $MPLAYER = "/usr/bin/mplayer";
    008 
    009 my $V = Videodir->new();
    010 
    011 my $CUI = Curses::UI::POE->new(
    012   -color_support => 1, 
    013   inline_states  => {
    014     _start => sub {
    015         $poe_kernel->delay('wake_up', 60);
    016     },
    017     wake_up => \&wake_up_handler,
    018 });
    019 
    020 my $WIN = $CUI->add(qw( win_id Window ));
    021 
    022 my $TOP = $WIN->add(qw( top Label 
    023   -y 0 -width -1 -paddingspaces 1 
    024   -fg white -bg blue
    025   ), -text => top_text());
    026 
    027 my $LBOX = $WIN->add(qw( lb Listbox
    028   -padtop 1 -padbottom 1 -border 1 ),
    029   -onchange    => \&selected,
    030   -onselchange => \&changed, 
    031 );
    032 
    033 my $BOTTOM = $WIN->add(qw( bottom Label
    034   -y -1 -width -1 -paddingspaces 1 
    035   -fg white -bg blue
    036   ), -text => bottom_text(),
    037 );
    038 
    039 $CUI->set_binding(sub { selected($LBOX) 
    040                       }, KEY_ENTER());
    041 $CUI->set_binding(sub { exit 0; }, "q");
    042 $CUI->set_binding(\&delete_confirm, "d");
    043 $CUI->set_binding(\&keep, "*");
    044 
    045 redraw(); # draw inital listbox content
    046 $CUI->mainloop;
    047 
    048 ###########################################
    049 sub ttl_icon {
    050 ###########################################
    051   my($ttl) = @_;
    052   return $ttl <  0 ? "!" : 
    053          $ttl <= 5 ? " " : "*" ;
    054 }
    055 
    056 ###########################################
    057 sub changed {
    058 ###########################################
    059     $BOTTOM->text(bottom_text());
    060 }
    061 
    062 ###########################################
    063 sub selected {
    064 ###########################################
    065   my $cmd = "$MPLAYER " . 
    066             active_item()->{path} . 
    067             ">/dev/null 2>&1";
    068   `$cmd &`;
    069 }
    070 
    071 ###########################################
    072 sub bottom_text {
    073 ###########################################
    074   my $item = active_item();
    075 
    076     # Work around PGdown bug
    077   return unless defined $item;
    078 
    079   my $str = sprintf "%d/%d | %.1f days" .
    080     " old | %s GB | TTL %s", 
    081     $LBOX->get_active_id() + 1,
    082     scalar @{$V->{items}},
    083     $item->{age}, $item->{size},
    084     $item->{ttl};
    085 
    086   return $str;
    087 }
    088 
    089 ###########################################
    090 sub wake_up_handler {
    091 ###########################################
    092     $V->rescan(); # Get newly added files
    093     redraw();
    094 
    095     redraw() if $V->shrink();
    096         # Re-enable timer
    097     $poe_kernel->delay('wake_up', 60);
    098 }
    099 
    100 ###########################################
    101 sub top_text {
    102 ###########################################
    103     return "tv1.0 | " . $V->{total_size}
    104      . " GB total | $V->{max_gigs} GB max";
    105 }
    106 
    107 ###########################################
    108 sub delete_confirm {
    109 ###########################################
    110   my $item = active_item();
    111 
    112   my $yes = $CUI->dialog(
    113     -title     => "Confirmation required",
    114     -buttons   => ['yes', 'no'],
    115     -message => "Are you sure you want " .
    116              "to delete $item->{file}?",
    117     qw( -tbg white -tfg red -bg white
    118         -fg red -bbg white -bfg red ));
    119 
    120   if($yes) {
    121     $V->remove($item->{file});
    122     redraw();
    123   }
    124 }
    125 
    126 ###########################################
    127 sub redraw {
    128 ###########################################
    129   $LBOX->{-values} = 
    130     [ map { $_->{file} } @{$V->{items}} ];
    131 
    132   $LBOX->{-labels} = { 
    133     map { $_->{file} => 
    134       ttl_icon($_->{ttl}) . " $_->{file}"
    135     } @{$V->{items}}
    136   };
    137 
    138   $LBOX->draw(1);
    139   $TOP->text(top_text());
    140   $BOTTOM->text(bottom_text());
    141 }
    142 
    143 ###########################################
    144 sub keep {
    145 ###########################################
    146   my $it = active_item();
    147   $V->{meta}->{$it->{file}}->{keep} = 1000;
    148   $V->meta_save();
    149   $V->rescan();
    150   redraw();
    151 }
    152 
    153 ###########################################
    154 sub active_item {
    155 ###########################################
    156   return $V->{items}->[ 
    157       $LBOX->get_active_id() ];
    158 }

Tasteneindrücke

Die Zeilen 41 bis 43 legen noch weitere Tastaturkombinationen fest. Drückt der User ``q'', möchte er das Programm verlassen und tv bricht mit exit 0 ab. Die Taste ``d'' (Delete) löscht über die Funktion delete_confirm() (Zeile 108) die selektierte Videodatei von der Platte, aber nicht ohne den Benutzer noch einmal um eine Antwort im Bestätigungdialog zu bitten. Und erhält eine selektierte Datei ein Sternchen (*), wird die Funktion keep aufgerufen, um in der Metadatenbank die TTL der Datei auf 1000 zu setzen, damit sie in naher Zukunft nicht gelöscht wird. Die Methode ttl_icon ab Zeile 49 bestimmt, wie die GUI Videos mit verschiedenen TTLs unterschiedlich darstellt. Ist die TTL kleiner als Null, die Datei also zum Löschen freigegeben, erscheint ein Ausrufezeichen, bei einer TTL kleiner als fünf Tagen gar nichts und sonst ein Sternchen.

Nach all diesen Vorbereitungen ist die GUI vollständig aufgesetzt. In Zeile 46 startet dann die mainloop des Moduls Curses::UI::POE, das den POE-Kernel mit dem damit verbundenen Multitasking-Reigen startet. Eine reinrassige POE-Anwendung dürfte keinerlei synchrone Plattenzugriffe ausführen, da tv aber nur hin und wieder schnell die Inode-Daten der Videodateien ausliest, kann das gerade noch angehen. Die GUI kann so zwar etwas ruckeln, aber nicht richtig einfrieren.

Ändern sich die Zustände im Videoverzeichnis, bekommt der wake_up_handler() dies beim nächsten periodischen Aufruf (spätestens nach 60 Sekunden) über die in Zeile 92 aufgerufenen Methode rescan des Moduls Videodirs.pm mit und frischt die innere Datenstruktur von Videodirs.pm auf. Diese Daten wandern dann in der anschließend aufgerufenen Funktion redraw() (ab Zeile 127) sofort in die angezeigte Listbox, deren Methode draw() eine Neuzeichnung der grafischen Darstellung veranlasst. Da sich eventuell auch die Anzahl der Dateien, der verbrauchte Plattenplatz und sogar der ausgewählte Eintrag ändern können, zeichnet redraw() die Kopf- und Fußbalken ebenfalls gleich neu.

Videoten aller Länder

Den Zugriff auf die Videodateien abstrahiert das Modul Videodir.pm. Das auf ~/tv voreingestellte Videoverzeichnis enthält nicht nur alle verfügbaren Video-Files, sondern auch eine Datei namens .meta, die das Verfallsdatum zu jeder Videodatei im in Abbildung 4 gezeigten YAML-Format speichert. Unter dem Schlüssel keep steht in .meta, wieviele volle Tage eine Datei aufgehoben wird, nachdem sie im Verzeichnis gelandet ist.

Das im Unix-Dateisystem gespeicherte Datum der letzten Modifikation einer Datei dient als Zeitstempel. Um die TTL (time to live) einer Datei auszurechnen, rechnet Videodir.pm in der Funktion age_in_days) zunächst die Differenz der gegenwärtigen Zeit und der Unix-mtime der Datei in Tagen aus. Die TTL ergibt sich dann aus der Differenz des in der Meta-Datei festgelegten keep-Wertes (in Tagen) vom Dateialter in Zeile 62.

Der Konstruktor new definiert ab Zeile 13 einige Defaultwerte für Konstanten, die sich aber vom Aufrufer überschreiben lassen. Erzeugt dieser zum Beispiel ein Objekt mit new(max_gigs =< 50), ist die Speicherplatzobergrenze nicht mehr 20, sondern 50 Gigabytes.

Die Methode rescan (Zeile 29) durchwandert sowohl das Videoverzeichnis (mit dem Glob $dir/*, das .meta nicht findet) als auch die Metadatei, die mit der Funktion LoadFile des YAML-Moduls eingelesen wird. rescan frischt die unter dem Schlüssel items gespeicherte interne Datenstruktur entsprechend dem aktuellen Zustand auf. Jedes Element im Array unter items ist eine Referenz auf einen Hash, der Werte zu den Schlüsseln file (Dateiname), path (absoluter Pfad), age (Alter in Tagen), size (Dateigröße in GB) und ttl (Zeit in Tagen bis der Löschschutz aufgehoben wird) enthält.

Neu entdeckten Dateien wird automatisch ein keep-Wert von 5 Tagen (Parameter keep_default) in der Meta-Datei zugewiesen. Am Ende von rescan() schreibt Videodir.pm die neuen keep-Werte mit meta_save() in die Metadatei zurück. Dateien, die seit dem letzten Scan von der Festplatte verschwunden sind, werfen die Zeilen 70-74 zuvor aus der Metadatei.

Abbildung 4: Die Metadaten in ~/tv/.meta im YAML-Format.

Ist die obere Marke der Plattenplatzbegrenzung überschritten, löscht die Methode shrink() solange freigegebenen Dateien, bis das Video-Verzeichnis sich wieder im Rahmen hält. Dabei filtert sie mit grep alle Einträge heraus, deren ttl-Eintrag kleiner Null ist. Da die Einträge im Array unter $self->items absteigend nach dem Datum sortiert sind (neueste Einträge kommen zuerst), wird diese Ergebnisliste mit reverse umgedreht, um die Dateien in der Reihenfolge ihrer Fälligkeit zu sortieren.

Ist die Obermarke noch nicht erreicht, kehrt shrink() tatenlos zurück und liefert den Wert 0. Dies nutzt die aufrufende Stelle im Skript tv, welche die dargestellte Listbox nur dann auffrischen muss, falls tatsächlich Dateien verschwunden sind.

Listing 2: Videodir.pm

    001 package Videodir;
    002 ###########################################
    003 use strict; use warnings;
    004 use YAML qw(LoadFile DumpFile);
    005 use File::Basename;
    006 
    007 ###########################################
    008 sub new {
    009 ###########################################
    010   my($class, %options) = @_;
    011 
    012   my $self = { 
    013     dir          => "$ENV{HOME}/tv",
    014     meta_file    => ".meta",
    015     keep_default => 5,
    016     meta         => {},
    017     max_gigs     => 20,
    018     %options };
    019 
    020   $self->{meta_path} = 
    021      "$self->{dir}/$self->{meta_file}";
    022 
    023   bless $self, $class;
    024   $self->rescan();
    025   return $self;
    026 }
    027 
    028 ###########################################
    029 sub rescan {
    030 ###########################################
    031   my($self) = @_;
    032 
    033   if(-f $self->{meta_path}) {
    034     $self->{meta} = 
    035         LoadFile($self->{meta_path});
    036   }
    037 
    038   $self->{total_size} = 0;
    039   my @items = ();
    040 
    041   my $dir = $self->{dir};
    042   for my $path (<$dir/*>) {
    043 
    044     next unless -f $path;
    045     my $file = basename $path;
    046 
    047     $self->{meta}->{$file}->{keep} = 
    048        $self->{keep_default} unless defined 
    049        $self->{meta}->{$file}->{keep};
    050 
    051     my $size = -s $path;
    052     $self->{total_size} += $size;
    053 
    054     my $age  = age_in_days($path);
    055 
    056     push @items, { 
    057       file => $file,
    058       path => $path,
    059       age  => $age,
    060       size => gb($size),
    061       ttl  => 
    062           $self->{meta}->{$file}->{keep} - 
    063           $age,
    064     };
    065   }
    066     
    067   $self->{total_size} =
    068       gb($self->{total_size});
    069 
    070     # Delete outdated entries
    071   for my $k (keys %{$self->{meta}}) {
    072      delete $self->{meta}->{$k} unless 
    073          -f "$self->{dir}/$k";
    074   }
    075 
    076   $self->meta_save();
    077 
    078     # Sort by descending by age
    079   $self->{items} = [
    080     sort { $a->{age} <=> $b->{age} } 
    081          @items ];
    082 
    083   return $self->{items};
    084 }
    085 
    086 ###########################################
    087 sub gb {           # Umrechnen in Gigabytes
    088 ###########################################
    089   my($val) = @_;
    090   return sprintf "%.1f", $val / (1024**3);
    091 }
    092 
    093 ###########################################
    094 sub remove {
    095 ###########################################
    096   my($self, $file) = @_;
    097 
    098   my $path = "$self->{dir}/$file";
    099 
    100   if(-f $path) {
    101     unlink $path or 
    102       die "Cannot unlink $path";
    103   }
    104   $self->rescan();
    105 }
    106 
    107 ###########################################
    108 sub age_in_days {
    109 ###########################################
    110     my($file) = @_;
    111 
    112     return(sprintf "%.1f", (time() - 
    113        (stat $file)[9]) / 24 / 3600);
    114 }
    115 
    116 ###########################################
    117 sub shrink {
    118 ###########################################
    119   my($self) = @_;
    120 
    121   my $deleted = 0;
    122 
    123   my @doomed = reverse
    124                grep { $_->{ttl} < 0 }
    125                @{$self->{items}};
    126 
    127   while($self->{total_size} > 
    128         $self->{max_gigs}) {
    129     last unless @doomed;
    130     my $item = shift @doomed;
    131     $deleted++;
    132     $self->remove($item->{file});
    133   }
    134   return $deleted;
    135 }
    136 
    137 ###########################################
    138 sub meta_save {
    139 ###########################################
    140   my($self) = @_;
    141   DumpFile($self->{meta_path}, 
    142            $self->{meta});
    143 }
    144 
    145 1;

Mit Trick installiert

Bei der Version 0.95 des Moduls Curses::UI vom CPAN verhaspelte sich die Eventschleife mit den Tastatureingaben, deswegen steht unter [3] eine gepatchte Version bereit. Die Datei Videodir.pm sollte in einem Verzeichnis installiert sein, wo tv es findet, dann steht dem ungetrübten Fernsehgenuss nichts mehr im Wege.

Infos

[1]
Listings zu diesem Artikel: ftp://www.linux-magazin.de/pub/listings/magazin/2006/08/Perl

[2]
mplayer-Homepage, http://mplayerhq.hu

[3]
Gepatchte Version des Moduls Curses::UI http://perlmeister.com/errata/Curses-UI-0.95-patch-ms1.tar.gz

Michael Schilli

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