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