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.
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.
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.
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!
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 }
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.
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.
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;
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.
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. |