Samstag, 11. Oktober 2014

Erfahrungen beim Erweitern des XBMC Soma FM Add-ons

Beim Anpassen des XBMC (oder bald Kodi) SomaFM Add-ons habe ich ein paar Sachen über die XBMC Add-on API gelernt, die ich hier festhalten möchte. Die Dokumentation ist dürftig, aber die API ist nicht sonderlich kompliziert. Beim Stöbern nach Informationen muss man immerwieder aufpassen, ob sie die gefundene Information auf Add-ons, Skins oder XBMC selbst beziehen. Hier geht es um Add-ons.

Manifest

Die Basics sind in der Dokumentation hinreichend beschrieben. XBMC erwartet ein Manifest namens addon.xml, das Metadaten, Abhängigkeiten und den Verweis auf das eigentliche Python Script des Add-ons bereitstellt. Für Soma FM sieht das Manifest so aus:

Ein Directory aufbauen

Ich bleibe beim Beispiel des Soma FM Add-ons und somit bei einem Musik oder Audio Add-on. Wählt der Nutzer das Add-on in der Liste seiner Musik Add-ons aus, startet XBMC das in der addon.xml angegebene Python Script. Das Script hat nun die Aufgabe, ein Verzeichnis mit Inhalten aufzubauen, das dann dem Nutzer in der GUI präsentiert wird

XBMC übergibt beim Start drei Parameter: Wie üblich beschreibt der erste Parameter das verwendete "Startkommando", also den Namen des Scripts selbst. Das ist in diesem Fall eine URL der Form plugin://addon_id, in unserem Fall also plugin://plugin.audio.somafm. Das ist nützlich, wenn man sich nicht darauf verlassen möchte, dass das Add-on seine ursprüngliche ID behält. Der zweite Parameter ist ein Handle. XBMC erwartet dieses Handle, wenn das Script mit dem XBMC Framework interagieren will. Der dritte Parameter ist ein Query String. Das kann relevant sein, wenn das Script unter anderen Umständen gestartet wird als beim Aufbau des Verzeichnisses, z.B. zum Auflösen eines Inhalts oder zum Ausführen einer Settings-Aktion. Wie der erste Parameter bereits andeutet, startet man das Add-on-Script über eine URL. Der Teil hinter einem eventuell vorhandenen ? ist dieser Query-Teil.

Um das Verzeichnis aufzubauen, ruft man für jeden Eintrag xbmcplugin.addDirectoryItem() auf: Dafür benötigt man das bereits erwähnte Handle, damit XBMC den Aufruf dem eigenen Add-on (bzw. dem richtigen Verzeichnisaufbau) zuordnen kann. Der Parameter url bekommt eine URL des eigentlichen Inhalts. Das kann eine lokale Audiodatei sein, eine Ressource im Internet (XBMC schluckt auch Playlisten usw., einfach mal ausprobieren) oder eine Plugin-URL. Davon mache ich hier Gebrauch und trage die als Kommandozeilenparameter übergebene Plugin-URL ein gefolgt von der für mich relevanten ID des konkreten Eintrags. Diese ID landet in diesem Fall als Pfad in der URL, nicht als Query-String. Das ist letztendlich Geschmackssache. Da ich hier eine Ressource beschreiben möchte, finde ich diesen Ansatz schöner. Der Parameter totalItems ist optional. Gibt man ihn an, zeigt XBMC beim Laden einen Fortschrittsbalken an.

Der Parameter listitem erhält ein xbmcgui.ListItem Objekt mit Daten, die für die Anzeige des Eintrags relevant sind: Die Konstruktorparameter erklären sich mehr oder weniger von selbst. Wieso (und ob) man hier noch mal die URL des Inhalts angeben muss, ist mir auch nicht ganz klar - aber so funktioniert's. ;) Darüberhinaus kann man noch Properties und Infos setzen. Die Property IsPlayable gibt an, ob der Eintrag einen abspielbaren Inhalt oder ein Unterverzeichnis repäsentiert. Info Labels sind relevant für Detailansichten und Sortierung.

Mit xbmcplugin.addSortMethod() kann man dem Verzeichnis Sortierungen hinzufügen, die auf den Info Labels arbeiten: Abschließend muss man xbmcplugin.endOfDirectory() aufrufen: Für beide Aufrufe wird wieder das Handle benötigt.

Dynamische Inhalte

Weiter oben schrieb ich, dass ich als URL für den Inhalt eines Verzeichniseintrags die Add-on-URL gefolgt von der ID des Eintrags setze. Konkret heißt das, dass z.B. der Soma FM Sender Groove Salad die URL plugin://plugin.audio.somafm/groovesalad hat.

In der ursprünglichen Implementierung des Add-ons wurde stattdessen direkt die URL einer der Playlists eingetragen, etwa http://somafm.com/groovesalad.pls. Das führte dazu, dass XBMC die Playlist als Unterverzeichnis öffnete, in dem der Nutzer dann einen konkreten Stream starten musste. Dieses m.E. unkomfortable Verhalten war der Hauptgrund, aus dem ich das Add-on bearbeitet habe. Zunächst passte ich das Script so an, dass statt der Playlist ein zufälliger Eintrag der Playlist verwendet wurde. Das hatte allerdings den Nachteil, dass beim Aufbau des Verzeichnisses alle Playlists heruntergeladen und ausgewertet werden mussten. Das dauerte dann leider "eine Weile" - obwohl der Nutzer womöglich nur ein oder zwei Sender wirklich hören wollte.

Wie gesagt habe ich nun eine URL eingetragen, die wieder auf das Plugin verweist. Im Script prüfe ich daher, ob es mit oder ohne eine Sender-ID aufgerufen wurde. Ist eine ID vorhanden, geht es offenbar nicht um den Aufbau des Verzeichnisses, sondern um das Abspielen eines Eintrags: In beiden Fällen muss am Ende xbmcplugin.endOfDirectory() aufgerufen werden. Zum Abspielen wird nun tatsächlich eine zufällige Stream-URL aus einer passenden Playlist gewählt und mit xbmcplugin.setResolvedUrl() in den Verzeichniseintrag gesetzt:

(Temporäre) Dateien

Playlists und die Senderliste speichere ich zwischen. Um einen geeigneten Ordner zu ermitteln, ruft man xbmc.translatePath() mit einer Special Path URL auf:

Eigene Funktionen aufrufen

Falls die temporären Daten mal vergriesgnaddelt sind, soll der Nutzer diese über die Einstellungen des Add-ons löschen können. Wie man Einstellungen baut, ist in der Dokumentation hinreichend beschrieben. Der Einstellungstyp action ermöglicht das Ausführen von XBMC- und Add-on-Funktionen. In meinen Settings gibt es folgenden Eintrag: In den Klammern von Runscript erkennt man die üblichen Aufrufparameter wieder: $ID verweist auf das eigene Plugin, 0 ist ein Dummy Handle und clearcache ist der Query Parameter. Er folgt zwar hier nicht dem üblichen Schema key=value, aber ich benötige das schlicht un ergreifend nicht.

Im Python Script muss man nun noch geeignet auf die Parameter reagieren: Damit der Nutzer merkt, dass auch etwas passiert ist, sollte man nach getaner Arbeit einen Hinweis anzeigen, z.B. mit xbmcgui.Dialog().notification(): Wie man erahnen kann, haben die Zahlen in den Settings bzw. in der Funktion xbmcaddon.Addon.getLocalizedString() etwas mit Lokalisierung zu tun.

Alle Quelldateien

Das große Ganze kann man auf Github betrachten, klonen oder forken.