Aus Linux-Magazin 08/2006

Digitaler Internet-Videorecorder in Perl

Nina Hagens Medien-Nutzungsverhalten zu kopieren ist für einen Auslandsdeutschen gar nicht einfach. Doch ein Internet-Fernsehportal, die abgekupferte Technik eines Geräts und Perl verhelfen Exilanten sogar zu zeitversetztem Pantoffelkino. Hier Gebliebene haben natürlich genauso viel Spaß.

Der von Onlinetvrecorder.com angebotene Service ist super. Dort kann jedermann kostenlos beliebige Sendungen des deutschen Fernsehprogramms aufzeichnen und runterladen. Zwar kränkelt die Website hin und wieder beim Recording und der Download geht nur tief in der Nacht einigermaßen zügig. Aber um im Ausland deutsches Fernsehen zu genießen, nehme ich das gern in Kauf.

Haben schließlich im Laufe einiger Wochen Dutzende Sendungen die Festplatte voll gemüllt, stellt sich die Frage nach einer Verwaltung(ssoftware). Die sollte mir eine Wahl aus den verfügbaren Sendungen gestatten und die alten Schinken, die ich eine Zeit lang nicht angerührt habe, von der Platte tilgen.

Hier drängt sich die Analogie zu einem Tivo auf. Der digitale Fernsehrekorder der gleichnamigen Firma und seine Klone sind fester Bestandteil amerikanischer TV-Gerätekultur – jedes Kind kennt die Marke. Die Geräte bieten eine einfach bedienbare Oberfläche, um Fernsehsendungen aufzuzeichnen und auf einer Festplatte zum späteren Sehgenuss zu speichern.

Damit sind nicht nur Werbeblöcke schnell durchquert, auch das so genannte Time-Shifting ist möglich: Mit massenweise eingelagerten Sendungen sieht der Konsument nicht mehr fern, wenn eine Sendung ausgestrahlt wird, sondern erst dann, wenn er Zeit dazu hat.

Abbildung 1 zeigt eine kleine Auswahl aufgezeichneter Programme auf meinem fünf Jahre alten (aufgebohrten) Tivo. Das Gerät nimmt Sendungen automatisch auf. Wegen des begrenzten Plattenplatzes löscht der Tivo alte Programme nach einigen Tagen selbsttätig, es sei denn, der Benutzer hat sie eigenhändig als »Save until I delete« markiert. Der Tivo unterscheidet zwischen Aufnahmen, die kurz vor dem Löschen stehen (Ausrufezeichen), mehrere Tage (keine Markierung) oder nur einen Tag (gelber Punkt) Gnadenfrist haben, und jenen, die er unbegrenzt aufbewahrt (grüner Punkt).

Abbildung 1: Eine Auswahl aufgezeichneter Fernsehprogramme auf dem digitalen Videorekorder Tivo.

Abbildung 1: Eine Auswahl aufgezeichneter Fernsehprogramme auf dem digitalen Videorekorder Tivo.

Das vorgestellte Skript »tv« bildet eine einfache Version dieser Benutzerschnittstelle nach. Statt der bekannten grafischen Toolkits wie Perl/Tk, GTK oder Wx-Widgets nutzt es die auf der Curses-Bibliothek fußende Widget-Sammlung Curses::UI. Mit ihr lassen sich typische GUI-Elemente wie Dialoge, Menüs oder Listboxen in einem Ascii-Terminal einfach programmieren. Der Look von 1980 ist wieder da – Nostalgie pur!

Die Videodateien erwartet das Skript in einem vorgegebenen Verzeichnis, das es alle 60 Sekunden durchforstet. Stellt es Änderungen fest, frischt es die Oberfläche auf. Auch prüft es laufend, ob die Gesamtheit aller Videodateien eine zulässige Größe überschreitet, voreingestellt sind 20 GByte. Ist dies der Fall, löscht es die ältesten Dateien – falls nicht anderweitig markiert – ohne nachzufragen von der Platte, bis die Höchstmarke wieder unterschritten ist.

An den Keyboards: Perl

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 [D]-Taste (Delete). Erhält der anschließend gezeigte Bestätigungsdialog (Abbildung 3) ein »Y« oder drückt der Bediener die [Return]-Taste, 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 dem automatischen Löschen des minütlich erscheinenden Plattenplatzkontrolleurs zu schützen, drückt der User die Taste [*] auf einem angewählten Listbox-Eintrag. Um die Sendung mit dem Mplayer abzuspielen, genügt es, [Return] auf der gewählten Datei zu drücken. Der unter [2] erhältliche Tausendsassa spielt alle gängigen Videoformate ab. Ein [Q] beendet einen einmal gestarteten Mplayer, 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].

Multitasking für Multimedia

Zunächst zieht Listing 1 die Module Curses::UI::POE und Curses rein, die beide auf dem CPAN erhältlich sind. Die praktischen Curses-Widgets enthält das Modul Curses::UI. Damit das Skript Multitasking-fähig ist, um zum Beispiel die periodischen Auffrischungsarbeiten auszuführen, definiert Curses::UI::POE eine abgeleitete Klasse, die das GUI in die Eventschleife des POE-Framework einbindet. POE kam im Snapshot schon öfter zu Ehren, meist um grafische GUIs mit kooperativem Multitasking ruckelfrei laufen zu lassen, obwohl das steuernde Programm aufreibenden Nebentätigkeiten nachgeht.

Abbildung 2: Das Perl-Skript »tv« bildet mit Curses::UI eine dem Tivo ähnliche Oberfläche nach.

Abbildung 2: Das Perl-Skript »tv« bildet mit Curses::UI eine dem Tivo ähnliche Oberfläche nach.

Der in Zeile 10 aufgerufene Konstruktor legt mit der Option »color_support« fest, dass das 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 der POE-Kernel nach exakt 60 Sekunden den Zustand »wake_up« aufruft, der die ab Zeile 89 definierte Funktion »wake_up_handler« ablaufen lässt.

Dort untersucht die Methode »rescan()« des Moduls Videodir (siehe unten) das Videoverzeichnis und notiert sich die Namen aller Dateien und deren Datumsstempel. An derselben Stelle liegt eine kleine Datenbank, in der steht, wie lange der Benutzer die einzelnen Filme behalten möchte. Das alles liest »Videodir::rescan()« ein und speichert es in einer internen Datenstruktur, die die anschließend gerufene Funktion »redraw()« holt und die Listbox des GUI aktualisiert.

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

Schlechte ins Kröpfchen

Die in Zeile 94 gestartete Methode »Videodir::shrink()« schrumpft das Videoverzeichnis durch Löschen alter Filme, falls deren Summe einen eingestellten Wert überschreitet. Dem »wake_up_handler« bleibt nur noch, dem POE-Kernel über »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. Er widmet sich dann dem Abarbeiten der Benutzereingaben und dem Auffrischen des GUI.

Boxen aus Buchstaben

Ab Zeile 19 baut »tv« das Ascii-GUI auf. Die Methode »add()« fügt zunächst ein neues Widget vom Typ »Window« ein, das den gesamten Platz des gerade laufenden Terminals beansprucht. Dann erhält das Window-Objekt seinerseits drei Widgets, die »add()« der Reihe nach von oben nach unten ins GUI einlässt: den oberen Info-Balken », die Listbox » und den unteren Balken » (Abbildung 2).

Die ersten beiden »add()«-Parameter bestimmen 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 Video- Einträgen ist in »Curses::UI::Listbox« definiert. Die Option »-y« der »add()«-Methode legt die vertikale Position der Balken fest: »1« die oberste Reihe, »-1« die unterste, »-bg« die Hintergrundfarbe und »-fg« die Farbe der Beschriftung.

»-width -1« breitet die Infobalken über das gesamte Terminal aus. »-paddingspaces« erweitert die blauen Balken bis ans Zeilenende, selbst wenn der Beschriftungstext kürzer ist. Normal wäre es, die Parameter als Paare im Format »Key => Value« zu überreichen. Damit die Listings nicht überborden, kommt hier aber die platzsparende Schreibweise mit »qw(…)« zu Ehren, die Optionen im String an den Wortgrenzen voneinander trennt und als Liste weitergibt. Statt sich rücksichtslos auszubreiten lässt die Listbox mit »-padtop 1« und »-padbottom 1« oben und unten Platz für beide Balken. Mit »-border 1« umfängt ein dünner blauer Rahmen die Listbox.

Das Skript verarbeitet zwei Arten Listbox-Events: »-onselchange« und »-onchange«. Die erste kommt zum Zuge, sobald der User eine Cursortaste betätigt und so den Listbox-Cursor um eins weiterschiebt. In diesem Fall startet der Event die ab Zeile 56 definierte Funktion »changed«, die die Metadaten des markierten Videofile in der Fußzeile des GUI ausgibt.

Dort steht, das wievielte Element von wie vielen Dateien ausgewählt ist (zum Beispiel 1/74), wie alt die Datei ist, wie viel Speicherplatz sie beansprucht und wie lange sie noch zu leben hat, falls der User nichts unternimmt. »TTL 4.3« zum Beispiel bedeutet, dass die Time to live 4,3 Tage beträgt. Nach dieser Zeit ist die Datei ohne Nachfrage vogelfrei, falls die Speicherplatzsituation heikel wird.

Listbox mit Index

Welcher Eintrag in der Listbox gerade selektiert ist, erkennt die Methode »get_active_id()« des Listbox-Objekts », die den Index des korrespondierenden Listenelements zurückgibt. Das Modul »Videodir.pm« (siehe weiter unten) hält sich eine Datenstruktur, die jedem Eintrag in der Listbox über seine ID die Metadaten seines Videos zuordnet.

Der zweite von der Listbox verarbeitete Event ist »-onchange«. Ihn löst der User aus, wenn er auf einem selektierten Eintrag die [Enter]-Taste betätigt oder mit der Maus auf einen Eintrag klickt. Das ist für »tv« das Signal, dass der User das Video ansehen will. Zeile 67 ruft den Mplayer mittels Backticks und »&« im Hintergrund auf. Das ist wichtig, denn das GUI muss weiter Tastatureingaben verarbeiten, sonst friert es ein.

Zusätzlich zu dieser Callback-Definition der Listbox legt Zeile 38 fest, dass das Betätigen von [Return] die Funktion »selected()« auslöst. Das Makro »KEY_ENTER()« ist im Modul »Curses« definiert und bezeichnet die [Return]- oder [Enter]-Taste. Dank des zuvor definierten Onchange-Eventhandlers der Listbox geschähe dies zwar auch ohne die explizite »set_binding«-Anweisung – denn jede Auswahl eines Listbox-Eintrags löst einen Onchange-Event aus. Doch scheiterte dies, sobald der User denselben Eintrag nochmals auswählt.

Die Zeilen 40 bis 42 belegen weitere Tasten. Des TV-Konsums Müde drücken [Q], und »tv« bricht mit »exit 0« ab. Die Taste [D] löscht über »delete_confirm()« ab Zeile 107 die selektierte Videodatei, ringt dem Benutzer aber zuvor per Dialog eine Bestätigung ab. Verleiht er einer Datei ein Sternchen (»*«), setzt das die Funktion »keep« in Gang, die in der Metadatenbank die Datei-TTL das Löschen verhindernd auf 1000 Tage setzt.

Die Methode »ttl_icon« ab Zeile 48 sorgt dafür, dass das GUI die Videos je nach TTL unterschiedlich darstellt. Ist sie kleiner als null, die Datei also zum Löschen freigegeben, erscheint ein Ausrufezeichen, bei einer TTL kleiner als fünf Tage gar nichts und sonst ein Sternchen. Nach all den Vorbereitungen ist das GUI nun vollständig aufgesetzt.

In Zeile 45 startet die »mainloop« des Moduls Curses::UI::POE, das den POE-Kernel mit dem damit verbundenen Multitasking-Reigen auslöst. Eine reine POE-Anwendung dürfte keine synchronen Plattenzugriffe ausführen. Dass »tv« hin und wieder rasch die Inode-Daten der Videodateien ausliest, geht aber gerade noch an. Das GUI kann so zwar etwas ruckeln, aber nie richtig einfrieren.

Ändert sich etwas im Videoverzeichnis, bekommt der »wake_up_handler()« das beim nächsten periodischen Aufruf, also spätestens nach 60 Sekunden über die in Zeile 91 aufgerufene Methode »rescan()« des Moduls »Videodir.pm« mit. Er frischt dann die innere Datenstruktur des Moduls auf. Diese Daten wandern dank der anschließend aufgerufenen Funktion »redraw()« ab Zeile 125 umgehend in die angezeigte Listbox. Deren »draw()«-Methode zeichnet die grafische Darstellung neu. Da sich eventuell auch die Anzahl der Dateien und deren Plattenplatzbedarf oder sogar der ausgewählte Eintrag ändert, zeichnet »redraw()« die Kopf- und Fußbalken auch gleich neu.

Abbildung 3: Der Dialog zum Löschen einer Sendung erscheint nach dem Drücken der Taste [D] und nötigt dem Benutzer eine Bestätigung seines Ansinnens ab.

Abbildung 3: Der Dialog zum Löschen einer Sendung erscheint nach dem Drücken der Taste [D] und nötigt dem Benutzer eine Bestätigung seines Ansinnens ab.

Videoten aller Länder

Das Modul »Videodir.pm« in Listing 2 abstrahiert den Zugriff auf die Videodateien. Das auf »~/tv« eingestellte Verzeichnis enthält nicht nur alle Videofiles, sondern auch eine Datei ».meta«, die deren Verfallsdaten im YAML-Format speichert (Abbildung 4). Unter dem Schlüssel »keep« steht in ».meta«, wie viele volle Tage eine Datei nach ihrer Landung im Verzeichnis erhalten bleibt.

Listing 2:
»Videodir.pm«

001 ###########################################
002 package Videodir;
003 ###########################################
004 use strict; use warnings;
005 use YAML qw(LoadFile DumpFile);
006 use File::Basename;
007 
008 ###########################################
009 sub new {
010 ###########################################
011   my($class, %options) = @_;
012 
013   my $self = {
014     dir          => "$ENV{HOME}/tv",
015     meta_file    => ".meta",
016     keep_default => 5,
017     meta         => {},
018     max_gigs     => 20,
019     %options };
020 
021   $self->{meta_path} =
022      "$self->{dir}/$self->{meta_file}";
023 
024   bless $self, $class;
025   $self->rescan();
026   return $self;
027 }
028 
029 ###########################################
030 sub rescan {
031 ###########################################
032   my($self) = @_;
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   return $self->{items};
083 }
084 
085 ###########################################
086 sub gb {           # Umrechnen in Gigabytes
087 ###########################################
088   my($val) = @_;
089   return sprintf "%.1f", $val / (1024**3);
090 }
091 
092 ###########################################
093 sub remove {
094 ###########################################
095   my($self, $file) = @_;
096 
097   my $path = "$self->{dir}/$file";
098   if(-f $path) {
099     unlink $path or
100       die "Cannot unlink $path";
101   }
102   $self->rescan();
103 }
104 
105 ###########################################
106 sub age_in_days {
107 ###########################################
108     my($file) = @_;
109 
110     return(sprintf "%.1f", (time() -
111        (stat $file)[9]) / 24 / 3600);
112 }
113 
114 ###########################################
115 sub shrink {
116 ###########################################
117   my($self) = @_;
118 
119   my $deleted = 0;
120   my @doomed = reverse
121                grep { $_->{ttl} < 0 }
122                @{$self->{items}};
123   while($self->{total_size} >
124         $self->{max_gigs}) {
125     last unless @doomed;
126     my $item = shift @doomed;
127     $deleted++;
128     $self->remove($item->{file});
129   }
130   return $deleted;
131 }
132 
133 ###########################################
134 sub meta_save {
135 ###########################################
136   my($self) = @_;
137   DumpFile($self->{meta_path},
138            $self->{meta});
139 }
140 
141 1;
Abbildung 4: Die Metadaten in »~/tv/.meta« liegen im YAML-Format vor. Sie speichern das Verfallsdatum der Videodateien.

Abbildung 4: Die Metadaten in »~/tv/.meta« liegen im YAML-Format vor. Sie speichern das Verfallsdatum der Videodateien.

Das im Dateisystem gespeicherte Modifikationsdatum der Datei dient als Zeitstempel. Um die Time to live der Datei zu bestimmen, berechnet »Videodir.pm« in der Funktion »age_in_days« zunächst die Differenz der gegenwärtigen Zeit und der Unix-Mtime der Datei in Tagen. Die TTL ergibt sich dann aus der Differenz des in der Metadatei festgelegten »keep«-Werts (in Tagen) und dem Dateialter (Zeile 62). Der Konstruktor »new« definiert ab Zeile 13 einige Defaultwerte für Konstanten, die der Aufrufer aber überschreiben darf. Erzeugt er zum Beispiel ein Objekt mit »new(max_gigs => 50)«, ist die Speicherplatzobergrenze nicht mehr 20, sondern 50 GByte.

Die Methode »rescan« ab Zeile 30 durchwandert sowohl das Videoverzeichnis (mit dem Glob »/*«, das ».meta« nicht findet) als auch die Metadatei, die »rescan« mit der Funktion »LoadFile()« des YAML-Moduls einliest. Die Methode frischt die unter dem Schlüssel »items« gespeicherte interne Datenstruktur dem aktuellen Zustand entsprechend auf. Jedes Element im Array unter »items« ist eine Referenz auf einen Hash, der Werte zu diesen Schlüsseln enthält:

  • »file«: Dateiname
  • »path«: absoluter Pfad
  • »age«: Alter in Tagen
  • »size«: Dateigröße in GByte
  • »ttl«: Zeit in Tagen bis zum Aufheben des
    Löschschutzes

Neu entdeckte Dateien erhalten in der Metadatei automatisch einen »keep«-Wert von fünf Tagen (Parameter »keep_default«). Am Ende von »rescan« schreibt »Videodir.pm« die neuen »keep«-Werte mit »meta_save()« in die Metadatei zurück. Die Zeilen 71 bis 74 werfen zuvor Dateien aus der Metadatei, die seit dem letzten Scan von der Festplatte verschwunden sind.

Ist die Plattenplatzgrenze überschritten, löscht die Methode »shrink()« so lange freigegebene Dateien, bis das Videoverzeichnis 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 »->{items}« absteigend nach dem Datum sortiert sind (neue kommen zuerst), dreht »reverse« diese Ergebnisliste um, 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«, die die dargestellte Listbox nur auffrischen muss, falls tatsächlich Dateien verschwunden sind.

Mit Trick installiert

Bei der Version 0.95 des Moduls Curses::UI vom CPAN verhaspelte sich die Eventschleife mit den Tastatureingaben, daher steht unter [3] eine gepatchte Version bereit. »Videodir.pm« sollte in einem Verzeichnis installiert sein, wo »tv« sie findet. Dann steht dem ungebremsten Glotzgenuss nichts im Wege. (jk)

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]

Der Autor


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.

DIESEN ARTIKEL ALS PDF KAUFEN
EXPRESS-KAUF ALS PDFUmfang: 5 HeftseitenPreis €0,99
(inkl. 19% MwSt.)
LINUX-MAGAZIN KAUFEN
EINZELNE AUSGABE Print-Ausgaben Digitale Ausgaben
ABONNEMENTS Print-Abos Digitales Abo
TABLET & SMARTPHONE APPS Readly Logo
E-Mail Benachrichtigung
Benachrichtige mich zu:
0 Kommentare
Älteste
Neuste Beste Bewertung
Inline Feedbacks
Alle Kommentare anzeigen
Nach oben