Plugin-API-Referenz¶
Die Schnittstelle, für die ein Plugin geschrieben wird: die Felder von
plugin.yaml, die zwei gRPC-Dienste, die Backend und Server austauschen,
und die Brücke window.evey, die ein Oberflächenmodul in seinem Frame
bekommt. Die Schritt-für-Schritt-Anleitung, die sie benutzt, ist
Ein Plugin schreiben. Was jede Berechtigung erlaubt und was
der Installer ablehnt, steht in Berechtigungen und Slots.
Manifest¶
Jedes ZIP enthält auf oberster Ebene eine plugin.yaml (oder plugin.yml),
höchstens 1 MB groß.
name: my-plugin # Pflicht; eindeutig, kebab-case
version: 1.2.3 # Pflicht; semver, X.Y.Z oder X.Y.Z-label
kind: plugin # Pflicht; "plugin" oder "theme"
language: nodejs # Pflicht für ein Plugin; eine Laufzeit des Images
entrypoint: node index.js # Pflicht für ein Plugin; die Befehlszeile, die der Server startet
description: "..." # optional
author: "..." # optional
min_evey_version: "1.0.0" # optional; akzeptiert und ignoriert
permissions: # was Backend und Oberfläche aufrufen dürfen
- network
- media.read
external_fetch: # Pflicht mit "network"; nur reine Hostnamen
- api.themoviedb.org
capabilities: # die Rollen, die das Plugin zu erfüllen angibt
- metadata.provider
web_root: public/ # Pflicht, wenn frontend angegeben ist; als statische Dateien ausgeliefert
frontend:
- slot: home.section # einer der erlaubten Slots
src: public/front.js # muss unter web_root liegen
config_schema: # die Felder des Bereichs Konfiguration auf der Karte des Plugins
api_key:
type: password # wird nie an einen Client zurückgeschickt
label: API key # nicht angezeigt: das Panel beschriftet Felder mit dem Schlüssel
required: true
city:
type: string
default: Paris
description: Resolved through geocoding
| Feld | Regel |
|---|---|
name, version, kind |
Pflicht. version muss X.Y.Z oder X.Y.Z-label entsprechen; ein Update muss eine höhere Version tragen als die installierte. |
language |
Pflicht für kind: plugin: python, nodejs, php, ruby, lua, bash, sh oder go. Es bestimmt die Prüfung der Laufzeit und die Stubs, die neben den Einstiegspunkt kopiert werden; es startet selbst nichts. |
entrypoint |
Pflicht für kind: plugin. Das erste Wort ist der Interpreter (oder die Binärdatei, mit native-binary), der Rest sind seine Argumente; der Prozess läuft im Verzeichnis des Plugins. |
permissions |
Jeder Wert muss im Katalog der Berechtigungen stehen. network verlangt ein nicht leeres external_fetch. |
external_fetch |
Hostnamen. Ein Schema und ein abschließender Pfad werden beim Einlesen entfernt; Hosts müssen exakt passen, ein Platzhalter *.example.com wird bei der Installation abgelehnt und ein Port-Suffix wird ignoriert; eine IP-Adresse lässt die Installation scheitern (Regeln). |
capabilities |
metadata.provider, media.read, media.write, webhook.receiver oder notification.sender; alles andere wird abgelehnt. Ein Theme darf keine angeben. |
web_root |
Pflicht, wenn frontend nicht leer ist; relativ, kein .., kein Backslash. . liefert das ganze Plugin-Verzeichnis aus und schreibt nur eine Warnung ins Protokoll. |
frontend[] |
slot muss in der Liste der Slots stehen; src muss unter web_root liegen. |
config_schema |
Eine Zuordnung von Feldname zu {type, label, description, required, default, min, max, options}; label wird nicht angezeigt, das Panel beschriftet Felder mit dem Schlüssel. Typen: string, text, password, boolean, integer, number, select (mit options: [{value, label}]). |
Ein Theme (kind: theme) darf language, entrypoint, permissions oder
capabilities nicht angeben. Sein Format steht unter Themes.
Geheimnisse. Ein Feld mit type: password oder mit secret: true wird
wie üblich gespeichert und an das Backend übergeben, kommt aber aus jeder
Leseroute maskiert zurück, auch im Bereich Konfiguration der Verwaltung.
Wer den Bereich mit unveränderter Maske speichert, behält den gespeicherten
Wert. Das Ändern eines anderen Feldes löscht also nie ein Geheimnis.
Backend: die zwei Dienste¶
Der Backend-Prozess und der Server kommunizieren per gRPC über zwei Unix-Sockets, deren Pfade in der Umgebung ankommen:
EVEY_PLUGIN_ID=<plugin id>
EVEY_PLUGIN_SOCK=<path> # Ihr Prozess lauscht hier und bedient PluginService
EVEY_HOST_SOCK=<path> # Ihr Prozess verbindet sich hierhin, um HostService aufzurufen
Der Server erkennt den Prozess an seinem Socket über die PID, die der Kernel
meldet (SO_PEERCRED); der Prozess bekommt keinen Token. Die Aufrufe sind
auf 600 pro Minute und Plugin begrenzt. proto/plugin/v1/plugin_api.proto
ist die Schnittstellendefinition. Stubs für die mitgelieferten Sprachen werden beim Start des
Plugins neben den Einstiegspunkt kopiert
(Voraussetzungen).
PluginService: was der Server aufruft¶
| Methode | Wann |
|---|---|
Initialize(plugin_id, config, host_version) |
Sobald der Prozess seinen Socket gebunden hat. config enthält die Werte aus config_schema, die der Administrator gesetzt hat. Antworten Sie success: false mit einem error, um den Start abzulehnen. |
HealthCheck() |
Regelmäßig, um einen lebenden Prozess von einem hängenden zu unterscheiden. Antworten Sie healthy: true, solange Sie arbeiten können. |
Shutdown(reason) |
Wenn das Plugin deaktiviert, aktualisiert oder deinstalliert wird oder der Server stoppt. Beenden Sie sich kurz nach der Antwort. |
OnEvent(event_type, payload, source) |
In der Schnittstelle definiert, aber der Server liefert Plugins heute keine Ereignisse: Setzen Sie es so um, dass es handled: true antwortet, und rechnen Sie mit keinem Aufruf. |
GetCapabilities() |
In der Schnittstelle definiert; der Server ruft es heute nicht auf. |
HostService: was das Plugin aufrufen kann¶
Ein Aufruf, dessen Berechtigung das Manifest nicht angibt, wird mit
PermissionDenied abgelehnt. Methoden mit dem Vermerk nicht bedient
antworten Unimplemented. Sie gehören zur Schnittstelle für eine spätere Version
und stehen hier, damit ein Stub Sie nicht in die Irre führt.
| Methode | Berechtigung | Hinweise |
|---|---|---|
Log(level, message, fields) |
keine | Ins Protokoll des Plugins geschrieben, sichtbar unter Protokolle auf seiner Karte. |
GetConfig(key) / SetConfig(key, value) |
keine | Die Konfiguration des Plugins: die Werte aus dem Bereich Konfiguration plus Schlüssel, die das Plugin selbst schreibt. Höchstens 256 Schlüssel, Schlüssel bis 256 Byte, Werte bis 16 KiB. |
GetData, SetData, DeleteData, ListDataKeys |
keine | Der Datenspeicher unten. |
EmitEvent(event_type, payload) |
keine | Auf dem Ereignisbus des Servers als plugin.event.<event_type> veröffentlicht, wo Webhooks es abonnieren können. |
GetMediaItem(id), ListMediaItems(library_id, type, parent_id, limit, offset) |
api.library oder media.read |
Der vollständige Datensatz des Mediums, mit file_path, provider_ids und genres. |
Fetch(url, method, headers) |
network |
Die einzige Verbindung aus der Sandbox nach außen: http oder https, GET oder HEAD, ein Host aus external_fetch, private Adressen nach der Auflösung und bei Weiterleitungen abgelehnt. Der Inhalt ist auf 8 MiB begrenzt (truncated zeigt es an), die ganze Anfrage auf 20 Sekunden. Der Server hängt keine eigenen Zugangsdaten an. |
SearchMedia, GetMediaStreams, GetChapters, GetSubtitles |
api.library oder media.read |
Nicht bedient. |
UpdateMetadata |
api.metadata oder media.write |
Nicht bedient. |
GetUserPreferences |
api.user |
Nicht bedient. |
SendNotification |
notification.sender |
Nicht bedient. |
Datenspeicher¶
Jedes Plugin hat einen dauerhaften Schlüssel-Wert-Speicher, ohne dass es eine Berechtigung angeben muss: Cursor, Caches, der Zeitpunkt der letzten Synchronisierung, alles, was einen Neustart überdauern soll. Werte sind opake Zeichenketten; JSON kodieren Sie selbst. Die ID des Plugins kommt vom authentifizierten Socket. Ein Plugin liest und schreibt also nur seinen eigenen Namensraum, und der ganze Speicher wird mit dem Plugin gelöscht.
| Methode | Zweck |
|---|---|
SetData(key, value) |
Einen Wert anlegen oder ersetzen. |
GetData(key) |
Liefert value und found. |
DeleteData(key) |
Einen Schlüssel entfernen. |
ListDataKeys() |
Die Schlüssel des Plugins. |
Dieser Speicher gilt für das ganze Plugin. Daten pro Benutzer (eine
Einstellung der Person, die das Widget ansieht) liegen auf der Seite der
Oberfläche, über die Berechtigung prefs unten.
Frontend-API¶
Ein Oberflächenmodul läuft in einem Frame mit opakem Ursprung (opaque origin),
unter der Richtlinie default-src 'none': kein Zugriff auf die Hostseite,
ihre Cookies oder ihren Speicher, kein Formular, kein Popup, und ein
connect-src, das auf die Hosts aus external_fetch über https begrenzt
ist. Alles andere läuft über window.evey, das vor dem Modul eingefügt
wird. Dasselbe Modul läuft unverändert in der Web-App und in den Apps für
Handy und Fernseher.
evey.onInit(callback)¶
Wird einmal mit dem Kontext aufgerufen, den der Host schickt. Ein Callback, der nach dem Eintreffen des Kontexts registriert wird, wird sofort aufgerufen.
evey.onInit(function (data) {
// data.config - die Konfigurationswerte des Plugins (config_schema)
// data.context - der Host-Kontext
// data.locale - die aktive Sprache, z. B. "en-US"
// data.theme - "dark" oder "light"
// data.surface - "web", "mobile" oder "tv"
// data.suggestedHeight - nur home.section: die Höhe (px), die der Host empfiehlt
// data.viewportHeight - nur tv: die Höhe des Bereichs in dp
});
evey.navigate(path)¶
Führt die Host-Anwendung zu path. Nur die Präfixe /home, /library,
/media, /player, /settings und /search werden befolgt. Alles andere
(der Verwaltungsbereich, die Anmeldung, eine absolute URL) wird
stillschweigend verworfen.
evey.toast(level, message)¶
Zeigt eine kurze Meldung (Toast) in der Oberfläche des Hosts. level ist
"success", "error" oder "info"; message ist reiner Text, nach 200
Zeichen abgeschnitten; höchstens 3 Meldungen pro Sekunde werden angezeigt.
evey.resize(height)¶
Verlangt, dass der Slot height Pixel hoch ist, begrenzt auf 50 bis 800.
0 klappt den Slot zu: Ein Widget, das nichts zu zeigen hat, nimmt keinen
Platz ein. Ein Widget in home.section öffnet sich ohne Zutun mit
data.suggestedHeight: Der Host kennt die Oberfläche, in der Ihr Widget
angezeigt wird, und empfiehlt die Höhe, die ein Startseiten-Widget dort hat.
So erhalten alle Widgets dieselbe Größe. Ändern Sie die Größe nur, wenn
Sie einen guten Grund dafür haben.
evey.prefs.* (Berechtigung prefs)¶
Die Einstellungen des aktuellen Benutzers im eigenen Namensraum des Plugins, an (dieses Plugin, diesen Benutzer) gebunden durch den Token, den der Host für den Frame verwaltet. Jeder Aufruf gibt ein Promise zurück; fehlt die Berechtigung, wird es abgelehnt.
await evey.prefs.set("units", "celsius");
const { value, found } = await evey.prefs.get("units");
const all = await evey.prefs.list(); // [{ key, value }]
await evey.prefs.delete("units");
evey.media.* (Berechtigung public)¶
Eine sichere Projektion der Medien: ID, Titel, Typ, Jahr, Inhaltsangabe, URL des Posters. Nie ein Dateipfad, eine Größe, eine Anbieter-ID oder ein Stream.
const item = await evey.media.get(id);
const page = await evey.media.list({ libraryId });
const data = await evey.media.poster(id); // das Poster als data:-URL
poster liefert eine data:-URL, weil die Richtlinie des Frames verbietet,
ein Bild direkt vom Server zu laden. Nutzen Sie sie als Quelle eines <img>
oder als Textur.
evey.assets.read(path)¶
Liest eine der eigenen Dateien des Plugins aus web_root, über den
relativen Pfad, als data:-URL: Eine Datei, die das Backend zur Laufzeit
geschrieben hat, wird im Frame nutzbar, ohne Probleme mit fremden Ursprüngen.
Nur die eigenen Dateien des Plugins sind erreichbar.
Auf einem Fernseher¶
Die TV-App führt das Modul in einer WebView aus, und einiges, das im Browser gilt, gilt dort nicht mehr:
- Richten Sie die Größe nicht nach Ihrem eigenen Viewport. In der
WebView ist
window.innerHeightder Slot, den der Host Ihnen gerade zugeteilt hat. Richten Sie sich nachdata.viewportHeightoderdata.suggestedHeight. - Kein Fokus, kein Zeiger. Das Widget bekommt nie den Fokus des Steuerkreuzes, und es gibt kein Hover. Alles Interaktive gehört auf die Web- und Handy-Oberflächen.
- Halten Sie Text von den Rändern fern. Fernseher schneiden den Rand ab (Overscan): Lassen Sie an jeder Seite etwa 5 % der Breite und Höhe frei.
- Testen Sie WebGL auf einer echten Box. Ein Shader läuft auf der GPU
und dem Treiber der Box, nicht auf denen Ihres Rechners; halten Sie
Rauschkoordinaten begrenzt.
tests/plugins/weather-v2ist das Referenz-Widget für all das.
Slots¶
| Slot | Wo er erscheint |
|---|---|
home.section |
Ein Inhaltsbereich auf der Startseite |
home.hero |
Der Bereich des großen Banners auf der Startseite |
library.sidebar |
Ein Seitenleistenbereich in den Bibliotheksansichten |
media.detail.sidebar |
Ein Seitenleistenbereich auf der Detailseite eines Mediums |
player.overlay |
Eine Ebene über dem Videoplayer |
settings.user.appearance |
Der Bereich Darstellung in den Benutzereinstellungen |
sidebar.bottom |
Der untere Teil der Hauptnavigation |
admin.nav |
Ein Eintrag im Verwaltungsmenü, der die eigene Seite des Plugins öffnet |
admin.nav ist der einzige Slot im Verwaltungsbereich. Er fügt nichts in
bestehende Verwaltungsbildschirme ein: Er ergänzt einen Menüeintrag, der die
Seite des Plugins im selben Sandbox-Frame öffnet, auf einer Route nur für
Administratoren. Wer ihn verlangt, erscheint damit in der Übersicht der
Änderungen, die bestätigt werden müssen. Jeder andere Slot admin.*,
login.*, setup.* und profile.security.* wird bei der Installation
abgelehnt.
Capabilities¶
Eine Capability gibt die Rolle an, die das Plugin nach eigener Angabe erfüllt:
metadata.provider, media.read, media.write, webhook.receiver oder
notification.sender. Der Installer prüft den Namen, der Administrator
sieht die Liste in der Vorschau und jede Ergänzung beim Update, und das
Sicherheits-Dashboard hält sie fest. Der Server leitet heute nichts anhand
einer Capability weiter: Ein metadata.provider bekommt keinen Aufruf zur
Anreicherung, ein webhook.receiver keine eingehende Anfrage. Was ein
Plugin wirklich tun kann, entscheiden seine Berechtigungen.
Berechtigungen der Oberfläche¶
public und prefs werden wie die anderen unter permissions angegeben.
Sie gewähren dem Backend-Prozess nichts; sie schalten evey.media.* und
evey.prefs.* im Frame frei. Der Frame erhält nie einen Token: Der Host stellt
einen Token für fünf Minuten aus, begrenzt auf (Plugin, Benutzer), und
leitet die Aufrufe weiter. Der Server lehnt standardmäßig jede Methode ab,
die die Bereiche des Tokens nicht nennen
(ADR-0012).
Was ein Plugin nie verlangen kann
admin.*, auth.*, system.*, user.write und library.write sind
fest gesperrt: Ein Manifest, das sie nennt, wird abgelehnt, ein Token
kann sie nie tragen, und Verwaltungsmethoden lehnen einen Plugin-Token
ab, egal welche Bereiche er hat.
Beispiele¶
examples/plugins/ enthält funktionierende Plugins im aktuellen
Manifestformat: Vorlagen in Node.js und Python (nodejs-example,
python-example), einen Metadatenanbieter (metadata-example), einen
Webhook-Empfänger (webhook-receiver), ein Startseiten-Widget mit
Build-Schritt (weather), den Datenspeicher mit den Berechtigungen public
und prefs und dem Slot admin.nav (data-demo), einen Rundgang durch
alle Feldtypen von config_schema, mit einer admin.nav-Seite (config-demo), und zwei Themes
(theme-dark-blue, open-workshop). make plugin-zips packt sie alle.