Tutorial zum Nutzen des Ajax Features
-------------------------------------

So ein  Ajax Feature  ist inzwischen  eingebaut. Jetzt  sieht man  den Links zum
erstellen eines Timers  ([Aufzeichnen]) an der  Farbe an ob  dieser ein Konflikt
verursachen würde(rot) oder  nicht(grün). Da dies  einen ziemlichen Aufwand  für
xxv bedeutet, jeden einzelnen EPG Event darauf zu prüfen ob er sich mit  anderen
Timern überschneiden würde ,hab ich das jetzt mit Ajax realisiert.

Am  Anfang  werden  diese  Links  wie  gewohnt  normal  dargestellt.  Nach   dem
vollständigen Laden der Seite  geht ein Javascriptprogramm jeden  einzelnen Link
durch und stellt  eine Anfrage per  Ajax an den  Server. Als Antwort  bekommt es
entweder ein 'ok' oder die Timer mit deren Id sowie Priorität die einen Konflikt
verursachen würden.

Schauen wir  uns doch  mal schnell  das javascript  (records.js) dazu an. Zuerst
müssen wir dafür  sorgen das die  Links erst dann  durchgeparst werden wenn  die
Seite schon geladen wurde. Dies tun wir einfach mit dem Event 'onLoad':

      window.onLoad = window.setTimeout("checkrecord('recordlink')", 1000);

Die Verzögerung von einer Sekunde hab  ich mit Absicht drin gelassen, da  man ja
nie weiss was der Browser so alles im Hintergrund noch macht. Es wird also  nach
dem   Laden   der   Seite    sowie   einer   weiteren   Sekunde    die   routine
checkrecord('recordlink') aufgerufen. Der  Bezeichner 'recordlink' wird  genutzt
um alle Links zu finden die den entsprechenden Namen besitzen:

     <a href="?cmd=tnew&amp;data=840461701">
        <span name='recordlink' id='840461701' class='normal'>
        [Aufzeichnen]
        </span>
     </a>

Kommen wir nun zur eigentlichen JS Routine "checkrecord":

    function checkrecord (name) {
        var fnWhenDone = function (oXML, sData) {
            if(oXML.responseText != 'ok') {
                var text = oXML.responseText.split(':');
                document.getElementById(sData).className = 'problem';
                document.getElementById(sData).title = '<?% gettext('When you want ...: ') %?>' + text[0];
            } else {
                document.getElementById(sData).className = 'noproblem';
                document.getElementById(sData).title = '';
            }
         };

        var fnWorking = function (oXML, sData) {  };

        var tags = document.getElementsByName('recordlink');
        for (var i = 0; i < tags.length; i++) {
            var epgid = tags[i].id;
            var url = "cmd=conflict&data=" + epgid + "&ajax=text";
            var aconn = new XHConn();
            var hosturl = 'http://' + window.location.host;
            aconn.connect(hosturl, 'GET', url, fnWhenDone, fnWorking, epgid);
        }
    }

Da das gesamte  Ajaxsystem asynchron abläuft,  also man schickt  eine Abfrage ab
und wartet nicht bis die Antwort  kommt ... man lässt sich sozusagen  anklingeln
wenn die Anfrage fertig  ist und beendet dann  seine Arbeit. Das tollste  daran,
das gesamte Webinterface bleibt nicht  einfach stehen, sondern man kann  in alle
Ruhe damit weiterarbeiten.

Zuerst lässt man sich alle Links  geben die im Namen 'recordlink' stehen  haben.
Diese geht man nun einzeln durch, liesst die jeweilige EPGid aus dem Id tag  aus
und baut die Ajax url zusammen.

        var tags = document.getElementsByName('recordlink');
        for (var i = 0; i < tags.length; i++) {
            var epgid = tags[i].id;
            var url = "cmd=conflict&data=" + epgid + "&ajax=text";
        .....

Diese unterscheidet sich nur in  dem zusätzlichen Parameter 'ajax'. Diesen  muss
man  entweder auf 'text' oder 'xml'  setzen, damit beschreibt man das Format wie
Ajax antworten soll:
            ....
            var url = "cmd=conflict&data=" + epgid + "&ajax=text";
            ....

Am Ende wird ein XMLHTTP Interface erstellt das mit XXV schon mitgeliefert wird,
da wir für JEDE Anfrage eine  Instance benötigen müssen wir diese auch  schon in
der Schleife erstellen:

            ....
            var aconn = new XHConn();
            var hosturl = 'http://' + window.location.host;
            aconn.connect(hosturl, 'GET', url, fnWhenDone, fnWorking, epgid);
            ....

Spätestens jetzt fallen uns die ominösen Funktionen 'fnWhenDone, fnWorking' auf.
Diese  Funktionen  antworten  sozusagen  dem  Klingeln,  es  sind  in Javascript
geschriebene Callbacks die anspringen  wenn die Anfrage einen  bestimmten Status
erreicht hat. Wenn die Anfrage  noch in Arbeit ist wird  'fnWorking' aufgerufen.
Hier könnte  man z.B.  den Text  'Aufzeichnen' durch  'Loading...' ersetzen oder
eine tolle Progressbar  darstellen, der Fantasie  sind da kaum  Grenzen gesetzt.
'fnWhenDone' springt an wenn die Anfrage eine Antwort geliefert hat. In  unserem
Fall halt 'ok' oder z.B. '10:50', das würde bedeuten der Konflikt tritt mit  dem
Timer 10 auf der  eine Priorität von 50  hat. 'fnWhenDone' macht nichts  anderes
als dem Link die entsprechende klasse zuzuweisen und noch einen Informationstext
in dem title von dem Link schreibt:

        var fnWhenDone = function (oXML, sData) {
            if(oXML.responseText != 'ok') {
                var text = oXML.responseText.split(':');
                document.getElementById(sData).className = 'problem';
                document.getElementById(sData).title = '<?% gettext('When you want ...: ') %?>' + text[0];
            } else {
                document.getElementById(sData).className = 'noproblem';
                document.getElementById(sData).title = '';
            }
         };

Das wars dann auch schon wieder, jedenfalls ist Ajax ziemlich mächtig. Aber  man
muss auch zugeben, die Möglichkeiten sind begrenzt. Eine komplette GUI mit Hilfe
dieses Frameworks  zu schreiben  würde vom  hundertsen ins  tausende führen, ich
weiss  wovon  ich  spreche  ;) Aber  kleine  Sachen,  ohne  die gesamte  Webpage
nachladen zu müssen, kann man sehr schön damit realsieren.

cu und viel Spass damit
xpix


Tutorial zum Schreiben eines Plugins fuer XXV
---------------------------------------------

Da ja xxv ein sehr modulares  Konzept besitzt muessen nur einige Dinge  beachtet
werden um selber Plugins fuer XXV schreiben zu koennen. Ich versuche das Konzept
anhand des Grab Plugins zu erklaehren.  Dieses Plugin soll alle 5 minuten  einen
grab Befehl an das svdrp Interface vom vdr schicken. Ausserdem soll es  moeglich
sein mit einem Befehl sich jederzeit das Bild ansehen zu koennen.

1. Plugin erstellen:
-------------------

Zu aller Erst muss ein Name fuer das Plugin festgelegt werden. Alle Plugins  bei
xxv hoeren auf folgendes Namenschema:

 XXV::MODULES::<NAME> also
 XXV::MODULES::GRAB

Der Pfad lautet also nach perl Konfention: lib/XXV/MODULES/GRAB.pm, alle  Module
und Plugins befinden sich grundsaetzlich in dem lib-dir.

2. Standart Methoden:
---------------------

Um ein Plugin zu schreiben werden Standartmethoden in dem Plugin benoetigt damit
xxv weiss  welchen Namen  das Plugin  hat und  welche Befehle  auf diesem Plugin
ausgefuert werden koennen. Im  grunde sind das nur  2 an der Zahl:  new, module.
Hier mal ein winziges Beispiel ohne Funktion:

 package XXV::MODULES::GRAB;

 sub new {
	my($class, %attr) = @_;
	my $self = {};
	bless($self, $class);

    $self->{MOD} = $self->module;

	return $self;
 }

 sub module {
     my $obj = shift || return error ('No Object!' );
     my $args = {
         Name => 'GRAB',
     };
     return $args;
 }

 1;



new
---
typisches   perlmethode   damit    das   plugin   initialisiert    werden   kann
(XXX::MODULES::GRAB->new({options => 1}).  Diese Routine wird  vom startprogramm
xxv.pl automatisch aufgerufen nach dem Schema:

    use XXV::MODULES::GRAB;
    my $auto = XXV::MODULES::GRAB->new(
        -config => $Config,
        -dbh    => $DBH,
        -realbin=> $RealBin,
    );

Dabei schaut xxv.pl einfach in dem Ordner mit den Plugins lib/XXV/MODULES/*  und
startet bei allen die new Methode. Dabei wird immer als Argumente die  komplette
Konfiguration von der xxvd.cfg als Hash ($Config->{name} => $value)  uebergeben.
Ausserdem wird das fertig konnektierte Datenbankhandle mit uebergeben und  sowie
der genaue Pfad wo das Programm laeuft.

In unserem Beispiel sieht die new Methode in dem Module Grab folgendermassen aus:

 # ------------------
 sub new {
 # ------------------
	my($class, %attr) = @_;
	my $self = {};
	bless($self, $class);

	# who am I
    $self->{MOD} = $self->module;

    # all configvalues to $self without parents (important for ConfigModule)
    map {
        $self->{$_} = $attr{'-config'}->{$self->{MOD}->{Name}}->{$_}
    } keys %{$attr{'-config'}->{$self->{MOD}->{Name}}};

    # i need the svdrp module
    $self->{svdrp} = main::getModule('SVDRP');

    # Interval to read timers and put to DB
    Event->timer(
        interval => $self->{config}->{interval},
        cb => sub{
            $self->grab();
        },
    );

	return $self;
 }

Sieht schlimmer aus als  es ist ;) Also  am Anfang wird der  Classenname($class)
und die Argumente(%attr) uebergeben. Die Klasse wird dann mit bless als Modul in
Perl definiert. Am besten die ersten 3 Zeilen einfach so uebernehmen, dann  kann
nichts schiefgehen ;)

In der folgenden Zeile ...

	# who am I
    $self->{MOD} = $self->module;

werden einfach  die Metadaten  ueber die  Methode $self->module  in die Variable
$self->{MOD}  abgespeichert,  somit  weiss  das  Hauptprogramm  welche  Befehle,
Versionsnummer usw. zur Verfuegung stehen.  Zu dem genauen Syntax lese  bitte im
naechsten Kapitel weiter.

Die  naechsten Zeilen  extraieren die  wichtigsten Parameter  fuer unseren  grab
Befehl.

Die Monsterzeile tut nur eins,  alle Configvariablen werden in dem  Schema $self
->{name} =  value abgespeichert.  So das  das neue  Configmodul in  der lage ist
jederzeit ein reconfigure vor zu nehmen.

War doch bis jetzt ganz einfach oder? ;) Ok weiter im Text, natuerlich  brauchen
wir  ja noch  das wichtigste  naehmlich die  SVDRP Schnittstelle  damit wir  dem
telnet Interface vom vdr sagen koennen das wir jetzt einen grab machen wollen.

Wie kommt man nun  an die anderen Module  ran, eigentlich ganz einfach.  mit der
folgenden Zeile:

    # i need the svdrp module
    $self->{svdrp} = main::getModule('SVDRP');

Hier wird aus dem Hauptprogramm eine Routine mit dem Namen getModule aufgerufen.
Als Parameter uebergeben wir den Namen des Modules das wir benoetigen. In  $self
->{svdrp} steht dann hoffentlich eine Referenz auf das SVDRP Module und  koennen
ueber  dieses Interface  nach Lust  und Laune  Befehle an  das svdrp  Interface
schicken und die Antworten abwarten. zB.:

 my $erg = $self->{svdrp}->command('stat disk');

Am Ende kommt noch etwas sehr spannendes, wir wollen ja alle 5 Sekunden ein Bild
grabben:

    # Interval to read timers and put to DB
    Event->timer(
        interval => $self->{interval},
        cb => sub{
            $self->grab();
        },
    );

Die  Macht   von  xxv   gegenueber  z.B.   vdradmin  ist   ja  sein  asynchrones
Eventinterface,  mit  diesem  interface  kann  man  mehrere  Aufgaben   parallel
erledigen. Zum Beispiel liest ja xxv  die epg.data und stellt neue Eintraege  in
die Datenbank, gleichzeitig ruft es aber  auch die Routine grab alle 5  sekunden
auf um ein neues Bild von der DVB-Karte zu erhalten. Hiermit koennen also Events
voellig losgeloest von den anderen Events behandelt werden als das was sie  sind
... naehmlich  als ein  Ereigbnis ;)  Naehere Informationen  ueber dieses Klasse
Modul koennt ihr bei CPAN nachlesen und nutzen:

http://search.cpan.org/~jprit/Event-1.00/lib/Event.pod

3. module
---------------------
So das war es auch  schon, upps ... stimmt nicht  ganz. Wir muessen ja noch  die
Metainformationen fuer das Plugin aufbereiten damit das Hauptprogramm auch weiss
um welches Plugin es sich handelt, wer das ganze verbrochen hat, wie es von  dem
user gesteuert werden kann usw.

 # This module method must exist for XXV
 # ------------------
 sub module {
 # ------------------
    my $obj = shift || return error ('No Object!' );
    my $args = {
        Name => 'GRAB',
        Description => 'This module grab a picture from livestream.',
        Version => '0.01',
        Date => '06.09.2004',
        Author => 'xpix',
        Preferences => {
            xsize => {
                description => 'The x size from picture',
                default     => 320,
                type        => 'integer',
                required    => 'This is required',
            },
            ysize => {
                description => 'The y size from picture',
                default     => 240,
                type        => 'integer',
                required    => 'This is required',
            },
            file => {
                description => 'The place to save the image file',
                default     => '/tmp/live.jpg',
                type        => 'string',
                required    => 'This is required',
            },
            interval => {
                description => 'The interval to grab the image',
                default     => 300,
                type        => 'integer',
                required    => 'This is required',
            },
        },
        Commands => {
            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },
            gdisplay => {
                description => 'Display the picture',
                short       => 'gd',
                callback    => sub{ $obj->display(@_) },
            },
        },
    };
    return $args;
 }

Ganz wichtig dabei ist der Parameter Name => 'GRAB', ueber diesen Namen  koennen
andere Plugins dieses Grabmodul nutzen. Ohne diesen Namen wuerde getModule nicht
funktionieren.  Nehmen wir  zum Beispiel  ein Fernbedienungsinterface  das nach
jeder Bedienung auch einen neuen Grab  machen will, damit der User die  Reaktion
des vdr  auf seinen  tastendruck sofort  sehen kann.  Dieses Plugin  wuerde also
folgendes aufrufen:
    ...
    $obj->click('menu');
    my $grabmodule = main::getModule('GRAB');
    $grabmodule->grab();
    ...

Der Rest kann angegeben werden, muss aber nicht:
        Description => 'This module grab a picture from livestream.',
        Version => '0.01',
        Date => '06.09.2004',
        Author => 'xpix',

Preferences laesst diese Werte in der Configdatei und auch im Preferences Dialog
erscheinen. Somit ist auch das Modul Config in der Lage ein reconfigure von  dem
modul auszuloesen.

Kommen wir nun  zu dem Teil  wo das Hauptprogramm  endlich weiss welche  Befehle
Grab zur Verfuegung stellt. Das macht der Commands Parameter. Jedes Plugin  kann
logischerweise  auch  mehrere  Befehle  besitzen  und  dem  user  zur Verfuegung
stellen.

Hier also unser Beispiel:
        ....
        Commands => {
            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },
            gdisplay => {
                description => 'Display the picture',
                short       => 'gd',
                callback    => sub{ $obj->display(@_) },
            },
        },
        ....

Das Kommando womit  man eine bestimmte  Aktion ausfuhert ist  der Name zu  einem
Hash  der noch  die anderen  Parameter beinhaltet.  Sehen wir  uns ein  Kommando
genauer an:

            grab => {
                description => 'Grab a picture',
                short       => 'gr',
                callback    => sub{ $obj->grab(@_) },
            },

description:
    beschreibt was das Kommando am Ende tut ;)
short:
    Dies ist eine Kurzform des Befehls grab, in diesem Fall gr. Es wird von xxvd
    ueberprueft ob  dieser Shortcut  schon existiert  und gg.  angemeckert. Dann
    solltet ihr Euch eine anderen Shortnamen einfallen lassen.
callback:
    Dies ist die Aktion die am ende ausgefuehrt wird, also in unserem Fall  $obj
    ->grab(@_). Als Parameter werden  grundsaetzlich der watcher, console  sowie
    optionale zusatzparameter uebergeben.

4. grab
---------------------

Schauen wir uns also gleich mal die entsprechende GrabMethode genauer an:

    # ------------------
    sub grab {
    # ------------------
        my $obj = shift || return error ('No Object!' );
        my $watcher = shift;
        my $console = shift;

        # the svdrp module
        my $svdrp = $obj->{svdrp};

        # the command
        my $cmd = sprintf('grab %s jpeg 80 %d %d',
                $obj->{file},
                $obj->{xsize},
                $obj->{ysize},
        );

        my $erg = $svdrp->command($cmd);
        $console->message($erg)
            if(ref $console);
        return 1;
    }

Am Anfang seht ihr auch gleich was an diese Methode uebergeben wird, erstmal das
eigentliche GrabObject dann  der watcher und  die entsprwechende Console  (z.Zt.
HTTPD,Telnet oder Interface). Dann hole ich mir noch schnell das svdrpObjekt das
ich ja in der new Routine definiert habe.

In der  naechsten Zeile  definiere ich  den GrabBefehl  und schicke  ihn mit der
Methode command an das TelnetInterface  vom vdr. Der verarbeitet den  Befehl und
gibt Message aus die ich dann meinerseits an die Console von dem User schicke.

Das wars ;) Ist doch eigentlich einfach oder?

So ich bin gespannt  auf Eure Plugins, falls  ihr Hilfe braucht stehe  ich Euch
jederzeit per Mail xpix<at>xpix.de zur verfuegung.

cu
xpix


