Python Modul zum Steuern einer VDR-Instanz via SVDRP

  • Hallo,


    ist zwar noch sehr "Work in Progress", aber vielleicht will jemand mit mehr Python-Erfahrung ja schonmal draufschauen und Feedback geben. Gerne auch in Form von Pull-Requests:


    https://github.com/M-Reimer/pysvdrp


    Das Modul soll später einmal SVDRP komplett abbilden und in einem weiteren Ausbauschritt kommen auch Interfaces für wichtige Plugins dazu.


    Die Idee ist es möglichst einfach Zugriff auf VDR-Funktionen zu schaffen. Die Daten von SVDRP werden im Modul geparst und als Python-Objekte bereitgestellt. Umgekehrt ist es möglich Daten zu erzeugen und das von SVDRP erwartete Format wird beim Senden automatisch erzeugt.


    Test-Code und Beispiele fehlen aktuell noch. Reiche ich aber bei Gelegenheit nach.


    Aktuell eingebaut sind erste Schritte für das Verwalten von Kanälen. Hintergrund: Ich arbeite zusammen mit Copperhead an einem Web-Interface für VDR das ausschließlich via SVDRP angebunden werden soll. Mein Fokus ist hier erstmal auf Kanal-Management, weil das dem VDR definitiv noch fehlt.


    Als nächstes kommt dann EPG rein weil ich gerne via externem Daemon dem VDR zusätzliche EPG-Info verpassen will.

  • Das Modul soll später einmal SVDRP komplett abbilden und in einem weiteren Ausbauschritt kommen auch Interfaces für wichtige Plugins dazu.


    Die Idee ist es möglichst einfach Zugriff auf VDR-Funktionen zu schaffen.

    Eventuell kann da die Vorarbeit aus https://people.debian.org/~tschmidt/maemo/chinook/vdrpylib/ hilfreich sein, da hat sich schon mal jemand die Mühe gemacht die SVDRP-Daten für Timer, Aufnahmen usw. aufzuarbeiten.

    ist zwar noch sehr "Work in Progress", aber vielleicht will jemand mit mehr Python-Erfahrung ja schonmal draufschauen und Feedback geben. Gerne auch in Form von Pull-Requests:

    Was mir da als erstes ins Auge springt, ist dass die Datei bzw. der Socket für die Verbindung nicht explizit geschlossen werden, wenn die Instanz abgeräumt wird, wodurch das nur implizit über die Garbage Collection passiert - statt dem Low-Level Ansatz mit Sockets könnte man z.B. auch das telnet-Modul nehmen (das hat u.a. den Vorteil, dass man Timeouts für Abfragen nutzen kann, damit der Client nicht endlos hängt, wenn etwas schief geht) und die Nutzung eines Context-Manager ermöglichen, damit die Verbindung immer sauber abgeräumt wird, auch wenn eine nicht abgefangene Exception geworfen wird.

    Hintergrund: Ich arbeite zusammen mit Copperhead an einem Web-Interface für VDR das ausschließlich via SVDRP angebunden werden soll. Mein Fokus ist hier erstmal auf Kanal-Management, weil das dem VDR definitiv noch fehlt.

    Habt ihr euch da schon für ein bestimmtes Framework auf Python- bzw. JS-Seite entschieden?


    Ich habe für yaVDR mit fastapi fürs Backend und mit VueJS für eine Single-Page Application gespielt, die Authentifizierung mache ich über PAM - der User bekommt dann einen JWT, mit dem er auf die API zugreifen kann. Damit kommt man ohne Session-Management im Backend aus.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • Schau ich mir mal an. Ich hab aber für die Schnittstellen die ich brauche schon grobe Vorstellung wie ich sie haben will.


    Timeout ist normalerweise ein Socket Feature. Ich schaue mal nach ob man das nicht nur aktivieren muss.


    Python nutzt keinen Garbage Collector sondern Reference Counting. Aber ich rüste das Schließen trotzdem nach.


    Was das Webinterface angeht: React als Framework auf Clientseite. Flask und Connexion auf dem Server.

  • Wenn du das Token in einem HttpOnly-Cookie sendest, hat es den Vorteil, dass der Client sich nicht bemühen muss, das Token immer passend zu senden, weil der Browser das automatisch macht. Und bei HttpOnly kann das Javascript das Ding auch nicht abgreifen.

  • Timeout ist normalerweise ein Socket Feature. Ich schaue mal nach ob man das nicht nur aktivieren muss.

    Ja, das kann man auch über den Socket machen, in der telnetlib ist das halt schon hübsch verpackt.

    Python nutzt keinen Garbage Collector sondern Reference Counting. Aber ich rüste das Schließen trotzdem nach.

    Ja, intern arbeitet das mit Reference Counting, die Dokumentation spricht trotzdem von GC:

    Sockets are automatically closed when they are garbage-collected, but it is recommended to close() them explicitly, or to use a with statement around them.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • Hab das mit dem Timeout über "socket" gelöst. Es ist dort als Funktion direkt vorgesehen. Ich definiere hier 20 Sekunden Timeout vor. Das ist erstmal nur ein Vorschlag. Bei Bedarf kann die Konstante dafür gerne noch höher gesetzt werden.


    Der Nutzer kann bei Bedarf über svdrp.socket.settimeout() auch selber anpassen.


    Beim "__del__" setze ich das Timeout immer auf 5 Sekunden runter bevor ich das "QUIT" an den VDR sende. Eventuell auftretende Fehler ignoriere ich komplett und schließe zuletzt das "socket" mit "close()".


    Aktuell rüste ich das Löschen von Kanälen nach und dann gibt es ein erstes sehr kleines Beispielprogramm.


    "vdrpylib" habe ich angeschaut. Ich werde mal sehen ob ich da etwas übernehmen kann. Allerdings macht das mehr als "nur" SVDRP. Das wollte ich genau nicht. Mein Modul kann ggf. durchaus auch einen "entfernten" VDR steuern während vdrpylib auch auf Aufnahmen usw. direkt zugreifen können will.


    Irgendwo muss man Grenzen setzen und mein Modul soll SVDRP ansteuern und den Zugriff darauf vereinfachen (ggf. auch gerne für beliebte Plugins). Mehr aber nicht.

  • "delete_channel" ist jetzt eingebaut. Auch wenn der VDR es noch nicht kann wird hier schon eine channel id als Parameter angenommen um race-conditions zu vermeiden. Wenn ich nämlich die Kanalliste durchlaufe und beim Durchlaufen lösche wird es mit den Kanalnummern definitiv Probleme geben. Alternativ kann an "delete_channels" auch direkt ein "Channel"-Objekt übergeben werden. Die channel id holt sich die Funktion dann selber.


    Als Beispiel für ein erstes Tool habe ich mir dann das hier gebaut: https://github.com/M-Reimer/vdr-delete-obsoletes


    Löscht "OBSOLETE" Kanäle. Aktuell gerade mal 5 Codezeilen (+ 2 Imports) mit meinem Modul:


    https://github.com/M-Reimer/vd…ster/vdr-delete-obsoletes


    Ist erstmal als Demo gedacht, allerdings kann ich mir durchaus vorstellen das noch etwas aufzuhübschen. Zum Beispiel wäre es nicht ganz uninteressant eine Konfig-Datei zu haben in der z.B. angegeben wird ab welcher Kanal-Nummer gelöscht werden darf. So kann man in dem "Wust" den der VDR selbst gesucht hat automatisch löschen aber im vorher liegenden "sortierten" Bereich darauf verzichten um ggf. selber Hand anzulegen.

  • Wenn ich nämlich die Kanalliste durchlaufe und beim Durchlaufen lösche wird es mit den Kanalnummern definitiv Probleme geben.

    Man könnte die Liste umdrehen, so dass er das Löschen mit der höchsten Kanalnummer beginnt. Dann sollte es keine Probleme geben, solange in der Zwischenzeit niemand anderes dazwischen funkt:

    Code
    1. for channel in reversed(svdrp.list_channels()):
    2. if not channel.groupsep and channel.name.endswith(" OBSOLETE"):
    3. print("Deleting channel: ", channel.name, file=sys.stderr)
    4. svdrp.delete_channel(channel)


    Dann ist mir noch aufgefallen, dass readline() über Zeilen stolpern kann, die ungültige UTF-8 Zeichen enthalten (die der VDR einfach weiterreicht) weil in https://github.com/M-Reimer/py…r/pysvdrp/__init__.py#L26 nicht angegeben ist, was passieren soll, wenn das Dekodieren der Zeile mit UTF-8 fehlschlägt - entweder man fängt den Fehler in _recvmsg ab oder man müsste da zusätzlich sowas wie errors='backslashreplace' (vgl, https://docs.python.org/3/library/functions.html#open) setzen.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • Man könnte die Liste umdrehen, so dass er das Löschen mit der höchsten Kanalnummer beginnt. Dann sollte es keine Probleme geben, solange in der Zwischenzeit niemand anderes dazwischen funkt


    Ich bin generell nicht so ein Freund davon Kanäle über Nummern zu referenzieren. Klaus hat ja für die nächste VDR-Version bereits einen Fix dafür im GIT. Dann geht "DELC" auch "offiziell" mit Channel-ID. In der Zwischenzeit schiebe ich hier eben ein "LSTC" zwischen um von Kanal-ID auf Kanal-Nummer zu kommen. Sowie der VDR das selber kann überspringe ich das ab der entsprechenden VDR-Version.


    Dann ist mir noch aufgefallen, dass readline() über Zeilen stolpern kann, die ungültige UTF-8 Zeichen enthalten (die der VDR einfach weiterreicht) weil in https://github.com/M-Reimer/py…r/pysvdrp/__init__.py#L26 nicht angegeben ist, was passieren soll, wenn das Dekodieren der Zeile mit UTF-8 fehlschlägt - entweder man fängt den Fehler in _recvmsg ab oder man müsste da zusätzlich sowas wie errors='backslashreplace' (vgl, https://docs.python.org/3/library/functions.html#open) setzen.


    Interessanter Punkt. Frage ist aber natürlich wie es dann weitergeht wenn ich so einen String an den VDR zurücksenden will. Was wäre da dann der beste Weg? Eigentlich soll dann an den VDR zurückgehen was ich ursprünglich bekommen habe. Auch wenn es kaputt ist.

  • Eigentlich soll dann an den VDR zurückgehen was ich ursprünglich bekommen habe. Auch wenn es kaputt ist.

    Dann bleibt ja eigentlich nur Bytestrings vom Socket zu lesen und diese zwischenzuspeichern und das Decoding nur an den Stellen zu machen, wo man wirklich darauf angewiesen ist.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • Wo kann denn das deiner Ansicht nach vorkommen?

    Ich bin bei einigen Kanalnamen im Netz von Vodafone darauf gestoßen, (insbesondere wenn die --chartab Option nicht auf ISO-8859-9 gesetzt ist) und in den EPG-Daten scheint das auch mitunter vorzukommen.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • Hallo,

    das Thema "OBSOLETE" Kanäle löschen finde ich interessant und ich wollte das gleich ausprobieren. Leider läuft es bei mir nicht.

    Wenn ich nach der Zeile 87 einen print einbaue, kann man erkennen, dass er jedes mal an einer anderen Stelle crashed, so ca. um die Channel Zeile 400 rum. Also eher kein grundsätzliches Decoding Problem. Weiter reicht hier mein Python Wissen aber auch nicht …

    Mein System: Ubuntu 18.04, vdr 2.4.2 aktueller git Stand, Python 3.6.9

    Code
    1. # python3 vdr-delete-obsoletes
    2. Traceback (most recent call last):
    3. File "vdr-delete-obsoletes", line 23, in <module>
    4. for channel in svdrp.list_channels():
    5. File "/usr/local/lib/python3.6/dist-packages/pysvdrp-0.0.1-py3.6.egg/pysvdrp/channels.py", line 12, in list_channels
    6. File "/usr/local/lib/python3.6/dist-packages/pysvdrp-0.0.1-py3.6.egg/pysvdrp/__init__.py", line 106, in _recvlist
    7. File "/usr/local/lib/python3.6/dist-packages/pysvdrp-0.0.1-py3.6.egg/pysvdrp/__init__.py", line 87, in _recvmsg
    8. File "/usr/lib/python3.6/codecs.py", line 321, in decode
    9. (result, consumed) = self._buffer_decode(data, self.errors, final)
    10. UnicodeDecodeError: 'utf-8' codec can't decode byte 0xdc in position 2646: invalid continuation byte
  • Das ist genau das von mir geschilderte Problem, da muss eine Fehlerbehandlung stattfinden, weil der VDR Bytes für Zeichen liefern kann (in dem Fall vermutlich ein "Ü" in einem 8-Bit Encoding wie iso-8859-1 oder iso-8859-9), die nicht dem angekündigten UTF-8 Encoding entsprechen.

    yaVDR-Dokumentation (Ceterum censeo enchiridia esse lectitanda.)

  • VDR läuft mit UTF-8.

    Oder ist das der Fall mit den kaputten Zeichen in Kanalnamen?

    Dann müsste er ja immer am gleichen Kanal crashen, macht er ja aber nicht. Ich hänge mal den Output von mehreren Versuchen an, mit einem print(line) in __init__.py", nach line 87.