From 09d2fc850d3095537ff7ca36551c9c3fc72f2e4f Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Sat, 7 Feb 2026 17:23:29 +0100 Subject: [PATCH] Nightly: deterministic plugin loading and docs refresh --- addon/default.py | 31 +++++++++++++++++++++++++------ addon/plugin_interface.py | 7 ++++++- docs/DEFAULT_ROUTER.md | 3 ++- docs/PLUGIN_DEVELOPMENT.md | 9 ++++++++- docs/PLUGIN_SYSTEM.md | 26 ++++++++++++++++++++------ 5 files changed, 61 insertions(+), 15 deletions(-) diff --git a/addon/default.py b/addon/default.py index ee63d76..8f1bbe2 100644 --- a/addon/default.py +++ b/addon/default.py @@ -1230,11 +1230,16 @@ def _discover_plugins() -> dict[str, BasisPlugin]: except Exception as exc: xbmc.log(f"Plugin-Datei {file_path.name} konnte nicht geladen werden: {exc}", xbmc.LOGWARNING) continue - plugin_classes = [ - obj - for obj in module.__dict__.values() - if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin - ] + preferred = getattr(module, "Plugin", None) + if inspect.isclass(preferred) and issubclass(preferred, BasisPlugin) and preferred is not BasisPlugin: + plugin_classes = [preferred] + else: + plugin_classes = [ + obj + for obj in module.__dict__.values() + if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin + ] + plugin_classes.sort(key=lambda cls: cls.__name__.casefold()) for cls in plugin_classes: try: instance = cls() @@ -1245,7 +1250,21 @@ def _discover_plugins() -> dict[str, BasisPlugin]: reason = getattr(instance, "unavailable_reason", "Nicht verfuegbar.") xbmc.log(f"Plugin {cls.__name__} deaktiviert: {reason}", xbmc.LOGWARNING) continue - plugins[instance.name] = instance + plugin_name = str(getattr(instance, "name", "") or "").strip() + if not plugin_name: + xbmc.log( + f"Plugin {cls.__name__} wurde ohne Name registriert und wird uebersprungen.", + xbmc.LOGWARNING, + ) + continue + if plugin_name in plugins: + xbmc.log( + f"Plugin-Name doppelt ({plugin_name}), {cls.__name__} wird uebersprungen.", + xbmc.LOGWARNING, + ) + continue + plugins[plugin_name] = instance + plugins = dict(sorted(plugins.items(), key=lambda item: item[0].casefold())) _PLUGIN_CACHE = plugins return plugins diff --git a/addon/plugin_interface.py b/addon/plugin_interface.py index f8c266d..273a00b 100644 --- a/addon/plugin_interface.py +++ b/addon/plugin_interface.py @@ -4,7 +4,7 @@ from __future__ import annotations from abc import ABC, abstractmethod -from typing import List, Optional, Set +from typing import Any, Dict, List, Optional, Set, Tuple class BasisPlugin(ABC): @@ -12,6 +12,7 @@ class BasisPlugin(ABC): name: str version: str = "0.0.0" + prefer_source_metadata: bool = False @abstractmethod async def search_titles(self, query: str) -> List[str]: @@ -29,6 +30,10 @@ class BasisPlugin(ABC): """Optional: Liefert den Stream-Link fuer eine konkrete Folge.""" return None + def metadata_for(self, title: str) -> Tuple[Dict[str, str], Dict[str, str], Optional[List[Any]]]: + """Optional: Liefert Info-Labels, Art und Cast fuer einen Titel.""" + return {}, {}, None + def resolve_stream_link(self, link: str) -> Optional[str]: """Optional: Folgt einem Stream-Link und liefert die finale URL.""" return None diff --git a/docs/DEFAULT_ROUTER.md b/docs/DEFAULT_ROUTER.md index 61a2aed..06835c7 100644 --- a/docs/DEFAULT_ROUTER.md +++ b/docs/DEFAULT_ROUTER.md @@ -10,7 +10,7 @@ Dieses Dokument beschreibt den Einstiegspunkt des Addons und die zentrale Steuer - startet die Wiedergabe und verwaltet Playstate/Resume. ## Ablauf (high level) -1. **Plugin‑Discovery**: Lädt alle `addon/plugins/*.py` (ohne `_`‑Prefix) und instanziiert Klassen, die von `BasisPlugin` erben. +1. **Plugin‑Discovery**: Lädt alle `addon/plugins/*.py` (ohne `_`‑Prefix). Bevorzugt `Plugin = `, sonst werden `BasisPlugin`‑Subklassen deterministisch instanziiert. 2. **Navigation**: Baut Kodi‑Listen (Serien/Staffeln/Episoden) auf Basis der Plugin‑Antworten. 3. **Playback**: Holt Stream‑Links aus dem Plugin und startet die Wiedergabe. 4. **Playstate**: Speichert Resume‑Daten lokal (`playstate.json`) und setzt `playcount`/Resume‑Infos. @@ -35,6 +35,7 @@ Die genaue Aktion wird aus den Query‑Parametern gelesen und an das entsprechen - **Plugin‑Loader**: findet & instanziiert Plugins. - **UI‑Helper**: setzt Content‑Type, baut Verzeichnisseinträge. - **Playstate‑Helper**: `_load_playstate`, `_save_playstate`, `_apply_playstate_to_info`. +- **Metadata‑Merge**: Plugin‑Metadaten können TMDB übersteuern, TMDB dient als Fallback. ## Fehlerbehandlung - Plugin‑Importfehler werden isoliert behandelt, damit das Addon nicht komplett ausfällt. diff --git a/docs/PLUGIN_DEVELOPMENT.md b/docs/PLUGIN_DEVELOPMENT.md index 84e8c96..388ef97 100644 --- a/docs/PLUGIN_DEVELOPMENT.md +++ b/docs/PLUGIN_DEVELOPMENT.md @@ -6,6 +6,7 @@ Diese Doku beschreibt, wie Plugins im ViewIT‑Addon aufgebaut sind und wie neue - Jedes Plugin ist eine einzelne Datei unter `addon/plugins/`. - Dateinamen **ohne** `_`‑Prefix werden automatisch geladen. - Jede Datei enthält eine Klasse, die von `BasisPlugin` erbt. +- Optional: `Plugin = ` als expliziter Einstiegspunkt (bevorzugt vom Loader). ## Pflicht‑Methoden (BasisPlugin) Jedes Plugin muss diese Methoden implementieren: @@ -22,6 +23,7 @@ Wesentliche Rückgaben an die Hauptlogik: - `episodes_for(...)` → Liste von Episoden-Labels - `stream_link_for(...)` → Hoster-/Player-Link (nicht zwingend finale Media-URL) - `resolve_stream_link(...)` → finale/spielbare URL nach Redirect/Resolver +- `metadata_for(...)` → Info-Labels/Art (Plot/Poster) aus der Quelle - Optional `available_hosters_for(...)` → auswählbare Hoster-Namen im Dialog - Optional `series_url_for_title(...)` → stabile Detail-URL pro Titel für Folgeaufrufe - Optional `remember_series_url(...)` → Übernahme einer bereits bekannten Detail-URL @@ -35,6 +37,9 @@ Standard für Film-Provider (ohne echte Staffeln): - `popular_series` → `popular_series()` - `genres` → `genres()` + `titles_for_genre(genre)` - `latest_episodes` → `latest_episodes(page=1)` +- `new_titles` → `new_titles_page(page=1)` +- `alpha` → `alpha_index()` + `titles_for_alpha_page(letter, page)` +- `series_catalog` → `series_catalog_page(page=1)` ## Empfohlene Struktur - Konstanten für URLs/Endpoints (BASE_URL, Pfade, Templates) @@ -60,6 +65,7 @@ Standard: `*_base_url` (Domain / BASE_URL) - `einschalten_base_url` - `topstream_base_url` - `filmpalast_base_url` + - `doku_streams_base_url` ## Playback - `stream_link_for(...)` implementieren (liefert bevorzugten Hoster-Link). @@ -75,7 +81,8 @@ Standard: `*_base_url` (Domain / BASE_URL) 2. **Navigation**: `series_url_for_title`/`remember_series_url` unterstützen, damit URLs zwischen Aufrufen stabil bleiben. 3. **Auswahl Hoster**: Hoster-Namen aus der Detailseite extrahieren und anbieten. 4. **Playback**: Hoster-Link liefern, danach konsistent über `resolve_stream_link` finalisieren. -5. **Fallbacks**: bei Layout-Unterschieden defensiv parsen und Logging aktivierbar halten. +5. **Metadaten**: `metadata_for` nutzen, Plot/Poster aus der Quelle zurückgeben. +6. **Fallbacks**: bei Layout-Unterschieden defensiv parsen und Logging aktivierbar halten. ## Debugging Global gesteuert über Settings: diff --git a/docs/PLUGIN_SYSTEM.md b/docs/PLUGIN_SYSTEM.md index d4ac5b7..bf2db3f 100644 --- a/docs/PLUGIN_SYSTEM.md +++ b/docs/PLUGIN_SYSTEM.md @@ -17,6 +17,7 @@ Weitere Details: - `einschalten_plugin.py` – Einschalten - `aniworld_plugin.py` – Aniworld - `filmpalast_plugin.py` – Filmpalast +- `dokustreams_plugin.py` – Doku-Streams - `_template_plugin.py` – Vorlage für neue Plugins ### Plugin-Discovery (Ladeprozess) @@ -26,8 +27,9 @@ Der Loader in `addon/default.py`: 1. Sucht alle `*.py` in `addon/plugins/` 2. Überspringt Dateien, die mit `_` beginnen 3. Lädt Module dynamisch -4. Instanziert Klassen, die von `BasisPlugin` erben -5. Ignoriert Plugins mit `is_available = False` +4. Nutzt `Plugin = ` als bevorzugten Einstiegspunkt (falls vorhanden) +5. Fallback: instanziert Klassen, die von `BasisPlugin` erben (deterministisch sortiert) +6. Ignoriert Plugins mit `is_available = False` Damit bleiben fehlerhafte Plugins isoliert und blockieren nicht das gesamte Add-on. @@ -38,19 +40,29 @@ Definiert in `addon/plugin_interface.py`: - `async search_titles(query: str) -> list[str]` - `seasons_for(title: str) -> list[str]` - `episodes_for(title: str, season: str) -> list[str]` +- optional `metadata_for(title: str) -> (info_labels, art, cast)` ### Optionale Features (Capabilities) Plugins können zusätzliche Features anbieten: - `capabilities() -> set[str]` - - `popular_series`: liefert beliebte Serien - - `genres`: Genre-Liste verfügbar - - `latest_episodes`: neue Episoden verfügbar +- `popular_series`: liefert beliebte Serien +- `genres`: Genre-Liste verfügbar +- `latest_episodes`: neue Episoden verfügbar +- `new_titles`: neue Titel verfügbar +- `alpha`: A-Z Index verfügbar +- `series_catalog`: Serienkatalog verfügbar - `popular_series() -> list[str]` - `genres() -> list[str]` - `titles_for_genre(genre: str) -> list[str]` - `latest_episodes(page: int = 1) -> list[LatestEpisode]` (wenn angeboten) +- `new_titles_page(page: int = 1) -> list[str]` (wenn angeboten) +- `alpha_index() -> list[str]` (wenn angeboten) +- `series_catalog_page(page: int = 1) -> list[str]` (wenn angeboten) + +Metadaten: +- `prefer_source_metadata = True` bedeutet: Plugin-Metadaten gehen vor TMDB, TMDB dient nur als Fallback. ViewIt zeigt im UI nur die Features an, die ein Plugin tatsächlich liefert. @@ -62,6 +74,7 @@ Eine Integration sollte typischerweise bieten: - `search_titles()` mit Provider-Suche - `seasons_for()` und `episodes_for()` mit HTML-Parsing - `stream_link_for()` optional für direkte Playback-Links +- `metadata_for()` optional für Plot/Poster aus der Quelle - Optional: `available_hosters_for()` oder Provider-spezifische Helfer Als Startpunkt dient `addon/plugins/_template_plugin.py`. @@ -79,8 +92,9 @@ Als Startpunkt dient `addon/plugins/_template_plugin.py`. - Keine Netzwerkzugriffe im Import-Top-Level - Netzwerkzugriffe nur in Methoden (z. B. `search_titles`) - Fehler sauber abfangen und verständliche Fehlermeldungen liefern -- Kein globaler Zustand, der across instances überrascht +- Kein globaler Zustand, der über Instanzen hinweg überrascht - Provider-spezifische Parser in Helper-Funktionen kapseln +- Reproduzierbare Reihenfolge: `Plugin`-Alias nutzen oder Klassenname eindeutig halten ### Debugging & Logs