Skip to content

Modul API DE

Domekologe edited this page Jul 31, 2026 · 4 revisions

Modul-API

🌐 English · Deutsch

Alles, was ein Modul bei MediaForge registrieren kann, an einer Stelle. Die vollständige Referenz — mit einem lauffähigen Beispielmodul je Einstiegspunkt — steht in .examples/thirdparties/README.md im Repository; diese Seite ist die Übersicht dazu.

Ein Modul ist ein Ordner unter web/thirdparties/<name>/ mit einer Funktion register(app). Alles Folgende wird von dort aufgerufen.

Registrierungsfunktionen

Funktion Modul Was sie hinzufügt
register_thirdparty web.thirdparties.registry Das Modul selbst: Menüeintrag, Einstellungskarte, Dashboard-Widget, extra_settings-Felder, Ein/Aus-Schalter. Siehe „Wo eine Einstellungskarte landet" unten
register_provider providers Eine Inhaltsquelle (Seite) mit eigenen Serien-/Staffel-/Episoden-Klassen
register_search_source search Eine zusätzliche Quelle für die globale Suche
register_home_feed_source home_feed Zeilen und einen Chip für diese Quelle auf der neuen Startseite
register_home_panel home_panels Einen Knopf in der Leiste der Startseite, mit eigenem Feld und optionalem Zähler-Badge
register_hoster extractors Einen Hoster/Extractor, der eine Embed-URL zu einem Stream auflöst
register_site_mirrors mirrors Mirror-Domains einer Seite, mit automatischem Failover
register_cineinfo_source web.cineinfo.registry Eine Metadatenquelle neben TMDB
register_subtitle_source subtitle_sources Eine externe Untertitelquelle im Download-Pfad, neben der eingebauten OpenSubtitles-Abfrage
register_monitor_site web.uptime_monitor Eine Karte auf dem UpTime-Dashboard, geprüft wie die eingebauten Seiten
register_notification_channel web.thirdparties.registry Einen Benachrichtigungskanal neben Telegram/Pushover/Discord/ntfy
register_event_hook web.thirdparties.registry Einen Callback auf App-Ereignisse (Download fertig, Queue-Eintrag fehlgeschlagen, …)
register_background_worker web.thirdparties.registry Einen Worker-Thread, der mit dem Modul startet und stoppt
register_backup_category web.backup Eigene Daten im Voll-/Teil-Backup
register_sensitive_keys web.db Einstellungsschlüssel, die verschlüsselt gespeichert werden
register_ui_pref_key web.db Eine UI-Einstellung pro Benutzerkonto

register_restart_handler (web.restart) gehört nicht zu dieser API — die Funktion ist Core-intern und wird nur von web/app.py aufgerufen.

Hilfsfunktionen

Keine Registrierungen — das sind Core-Dienste, die ein Modul aufruft. Alles andere in web/, das mit einem Unterstrich beginnt, ist Core-intern und kann sich jederzeit ändern; wer so etwas importieren möchte, fragt besser nach einer öffentlichen Variante.

Funktion Modul Zweck
lookup_media web.tmdb_cache Gecachte, ratenbegrenzte TMDB-Abfrage für einen Titel oder eine IMDB-ID
is_tmdb_configured web.tmdb_cache Ob überhaupt ein TMDB-API-Key hinterlegt ist
get_setting / set_setting web.db Eine einzelne Einstellung lesen/schreiben
get_json_setting / set_json_setting web.db Eine Listen- oder Dict-Einstellung lesen/schreiben

TMDB-Metadaten (lookup_media)

from ...web.tmdb_cache import lookup_media

info = lookup_media("Dark", media_type="tv", require_confident=True)
if info:
    tmdb_id = info["tmdb_id"]
    plot    = info["overview"]

Liefert das Metadaten-Dict (tmdb_id, media_type, title, overview, genres, providers, fsk, vote_average, trailer_key, recommendations, raw_details) oder None, wenn TMDB nicht konfiguriert ist, nichts gefunden wurde oder das Ergebnis herausgefiltert wurde. Es gibt keinen {"found": False}-Fall, den man prüfen müsste.

  • media_type="movie" / "tv" verlangt genau diese Art von Treffer.
  • require_confident=True verlangt, dass der zurückgegebene Titel wirklich zum angefragten passt. Die TMDB-Suche antwortet auf fast jede Anfrage mit irgendetwas — also einschalten, sobald das Ergebnis angezeigt wird und nicht nur die ID benutzt wird.
  • API-Key, Anbieterland und UI-Sprache werden automatisch aufgelöst. cineinfo_tmdb_api_key selbst auszulesen, ist nicht nötig.

Ergebnisse werden 24 h gecacht und prozessweit ratenbegrenzt; die Funktion in einer Schleife aufzurufen, ist also in Ordnung. Sie macht blockierendes Netzwerk-I/O — größere Mengen gehören in einen register_background_worker, nicht in einen Request-Handler.

Listen-/Dict-Einstellungen (get_json_setting / set_json_setting)

from ...web.db import get_json_setting, set_json_setting

rooms = get_json_setting("module:my_mod:rooms", [])
rooms.append(name)
set_json_setting("module:my_mod:rooms", rooms)

json.dumps/json.loads rund um set_setting bitte nicht selbst bauen. Ein fehlender, leerer, ungültiger oder falsch geformter Wert wird geloggt und liefert den Standardwert zurück — eine kaputte Zeile liest sich damit als „nicht gesetzt", statt mitten im Request eine Ausnahme zu werfen. Der Standardwert wird kopiert, nie geteilt, und Umlaute bleiben lesbar gespeichert.

Jeder Einstellungsschlüssel wird für sich geschrieben — set_setting ist ein Upsert auf genau einen Schlüssel, das Speichern eines Werts kann also nie einen anderen leeren. Einen Sammel-Aufruf „alle meine Einstellungen schreiben" gibt es bewusst nicht.

Untertitelquellen (register_subtitle_source)

Der Download-Pfad sammelt Untertitel in drei Durchgängen: die Fassungen von yt-dlp, die Player-Konfiguration des Hosters und — nur für die Sprachen, die dann noch fehlen — eine externe Abfrage. OpenSubtitles.com ist die eingebaute Umsetzung dieses dritten Durchgangs; register_subtitle_source erlaubt es einem Modul, seinen eigenen Dienst (einen privaten Server, einen Fansub-Index, eine kostenpflichtige API) in denselben Schritt einzuhängen.

from ....subtitle_sources import register_subtitle_source

def fetch(video_path, have_langs, meta):
    # have_langs: ISO-639-2/B-Tags, die die Datei bereits hat -- diese nie
    # erneut laden. meta: {"query", "season", "episode", "imdb_id",
    # "tmdb_id"}, jeder Wert moeglicherweise None. Zurueckgegeben werden die
    # geschriebenen Sidecar-Dateien, benannt als <Videoname>.<Sprache>.<Endung>;
    # der bestehende Mux-Pfad nimmt sie von dort auf.
    if "ger" in have_langs:
        return []
    return []

register_subtitle_source(MODULE_ID, "myservice", "My Subtitle Service", fetch)
  • item_id ist die id, die das Modul bereits an register_thirdparty() übergeben hat. Die Registrierung wird darüber verwaltet, also entfernt unregister_module() sie automatisch, sobald das Modul deaktiviert oder deinstalliert wird; der Modulmanager führt die Fähigkeit als subtitle_source → „Untertitelquelle" auf.
  • source_id darf weder mit einer eingebauten Quelle (RESERVED_SOURCE_IDS = {"opensubtitles"}) noch mit der Quelle eines anderen Moduls kollidieren — beides löst eine Ausnahme aus.
  • fetch läuft im Queue-Worker zwischen Download und ffmpeg-Mux und hält damit genau diese Episode auf: ein paar HTTP-Anfragen mit kurzen Timeouts, mehr nicht. Die Funktion darf nicht werfen — Ausnahmen werden gefangen und geloggt, eine Quelle, die jedes Mal wirft, ist aber nur Ballast.
  • Gegenstücke: unregister_subtitle_source(item_id), thirdparty_subtitle_source_ids(), iter_subtitle_sources().

Referenzmodul: .examples/thirdparties/example_subtitle_source/.

Wo eine Einstellungskarte landet

settings_host wählt die Seite; die Seite wählt den Platz. settings_tab ist ein Wunsch, den der Host überstimmen darf — angewendet bei der Registrierung durch registry._placed_tab(), damit alles Nachgelagerte die tatsächliche Platzierung sieht:

settings_host Karte landet settings_tab
"integrations" (Standard) Immer auf dem Third-Party-Tab Wird ignoriert, stillschweigend
"notifications" Immer ein eigener Tab Eine eingebaute Kanal-Id oder der blanke Standard wird zu module_<item_id>; eine eigene Id bleibt
"monitoring" Immer ein eigener Tab Dieselbe Regel
"settings" Modulmanager → Modul-Einstellungen Wird ignoriert (die Seite gruppiert nach Host)

„Was haben meine Module dieser Seite hinzugefügt?" hat pro Seite genau eine Antwort — und hört auf, eine zu sein, sobald sich ein Modul auf dem CineInfo-Tab oder in Telegrams Panel verstecken kann. Ein Modul, das einen eigenen Tab will, wählt einen Host, der ihm einen gibt, keine Tab-Id.

Unabhängig davon steht jede Modul-Karte zusätzlich unter Modulmanager → Modul-Einstellungen, nach Host gruppiert. Keine Kopie: beide Stellen fahren über dieselbe /api/settings/thirdparty/<id>-API.

Secrets

Ein "secret"-Feld, ein Schlüssel in MODULE_SENSITIVE_SETTINGS und alles, was an register_sensitive_keys() übergeben wird, liegt verschlüsselt in der Datenbank, und die generische Settings-API liefert statt des Werts eine Maske (registry.SECRET_MASK) — ein PUT, das die Maske zurückschickt, bedeutet „unverändert". Das gilt für jeden Schlüssel, den MediaForge als sensibel kennt, nicht nur für Felder mit type="secret": ein als normales Textfeld angezeigtes Secret wird ebenfalls maskiert. Im Modul selbst ändert sich nichts, get_setting() entschlüsselt wie gewohnt. Wer einen solchen Wert auf einer eigenen Seite ausgibt, sollte es genauso halten — ein gespeichertes Secret gehört nie ins HTML.

Frontend-Bausteine

Statt eigenem CSS besser das vorhandene Vokabular nutzen:

  • Formular-Elemente (forms.css): .chb-main für Checkboxen, .toggle für Ein/Aus-Schalter, .mf-segmented, .mf-multiselect, .mf-chip.
  • Layout und Inhalt (mf_components.css, global geladen): .mf-search, .mf-toolbar, .mf-poster-grid, .mf-timeline, .mf-progress, .mf-empty, .mf-pagination-bar.
  • Farben (variables.css): niemals Hex-Werte hart schreiben. Status-Pills nutzen das Paar --success + --success-bg (analog --warning, --error, --info); beide Hälften sind je Theme definiert.
  • Multi-Select (mf_multiselect.js, auf jeder Seite geladen): data-mf-multiselect an ein .mf-multiselect-Wurzelelement schreiben — Öffnen/Schließen, das Zusammenfassungs-Label im Trigger, Klick nach außen bzw. Escape und die Positionierung (wird in scrollenden Containern nicht abgeschnitten) laufen dann von selbst, ohne Init-Aufruf, also auch für später per JS gerendertes Markup. Den Text über data-none-label / data-many-label / data-max-names steuern und auf mf-multiselect-change / mf-multiselect-close am Wurzelelement lauschen (detail: {values, labels}, bubbelnd). Helfer: window.mfMultiSelect.values(), .labels(), .refresh(), .open(), .close(), .closeAll().
  • Escaping (mf_escape.js, auf jeder Seite geladen): window.mfEscape() für alles, was ins HTML geht — quote-sicher, deckt also auch Attribute ab — und window.mfSafeUrl() für href/src. Keinen eigenen Escaper schreiben.
  • Polling (mf_poll.js): window.mfPoll(fn, ms) statt setInterval, damit der Timer pausiert, solange der Tab versteckt ist.

Übersetzungen

Modul-Templates verwenden {{ _('...') }} wie der Core; ein Modul bringt seinen eigenen Katalog unter <modul>/translations/<locale>/LC_MESSAGES/ mit. Nach dem Bearbeiten einer .po muss pybabel compile laufen — ohne diesen Schritt bleibt die Änderung zur Laufzeit wirkungslos.

register_home_panel — ein Knopf auf der Startseite

Die Startseite hat unter dem Suchfeld eine Knopfleiste und darunter ein Feld, dessen Inhalt vom gewählten Knopf abhängt (Warteschlange, Aktivität, Mediathek und — für Admins — Speicher und System). Ein Modul ergänzt seinen eigenen:

from mediaforge.home_panels import register_home_panel

def my_panel():
    return {
        "stats": [{"label": "Wartet", "value": "3", "tone": "warn"}],
        "items": [{"title": "Irgendwas", "sub": "vor 2 Min.",
                   "percent": 40, "href": "/mymodule", "tone": "ok"}],
        "link":  {"href": "/mymodule", "label": "Öffnen"},
        "empty": "Nichts zu tun.",
    }

register_home_panel(
    item_id="mymodule",          # die id aus register_thirdparty()
    panel_id="mymodule",         # eindeutig; die eingebauten ids sind reserviert
    label="Mein Modul",
    view=my_panel,               # wird nur beim Öffnen aufgerufen
    badge=lambda: 3,             # OPTIONAL, läuft bei jedem Startseitenaufruf
    badge_label="{} Aufträge warten",  # OPTIONAL, der Tooltip zum Badge
    admin_only=False,
    icon="M3 6h18M3 12h18",      # OPTIONAL, SVG-Pfaddaten für einen 24x24-Pfad
)

Was man wissen sollte:

  • Ein Badge ohne badge_label ist ein Rätsel. Der Text wird zum Tooltip des Knopfes und zu seinem zugänglichen Namen, {} wird durch die Zahl ersetzt. Der eingebaute System-Knopf kam ohne aus, und seine „58" wurde für eine Versionsnummer und für einen Fehlercode gehalten, bevor jemand herausfand, dass sie fehlgeschlagene Downloads zählt. Übersetzt schicken, wie label.
  • view ist faul, badge nicht. Der Feldinhalt wird geholt, wenn der Nutzer das Feld öffnet, und danach alle 20 s aktualisiert (nur solange der Tab sichtbar ist). Das Badge läuft bei jedem Aufruf der Startseite für jedes registrierte Feld — also ein billiger COUNT, niemals ein Netzwerkaufruf.
  • Text, kein Markup. Jeder String wird clientseitig escaped. Unbekannte Schlüssel fallen weg, percent wird auf 0–100 begrenzt, und href muss ein seiteninterner Pfad sein (/library); absolute oder protokollrelative URLs werden entfernt. Höchstens 12 Einträge und 6 Kennzahlen pro Feld.
  • action statt href für Modals. Manches ist keine Seite: die Warteschlange ist ein Modal aus base.html, eine Route /queue gibt es nicht. "action": "queue" öffnet den Warteschlangen-Hub; die Liste der erlaubten Aktionen ist fest (PANEL_ACTIONS), ein Feld kann also keine eigene JS-Funktion benennen.
  • Deine Texte übersetzt du selbst. Die eingebauten Felder schicken i18n-Schlüssel, die das Template auflöst; ein Modul schickt fertigen Text, weil der Core einen Text nicht übersetzen kann, den er nie gesehen hat.
  • admin_only=True wird serverseitig durchgesetzt, in der Liste und in der Feld-Route — ein Nicht-Admin bekommt 403, nicht nur einen versteckten Knopf.
  • Ein Feld, das eine Ausnahme wirft, zerlegt die Seite nicht: die Leiste funktioniert weiter, das Feld meldet sich als nicht verfügbar. Genauso bei einem Badge (zählt dann als 0).
  • Das Aufräumen läuft über item_id automatisch — wird das Modul deaktiviert, verschwindet der Knopf.

Welches Feld zuletzt offen war, steht am Konto — die Startseite kommt so zurück, wie man sie verlassen hat.

Clone this wiki locally