Nightly: deterministic plugin loading and docs refresh

This commit is contained in:
2026-02-07 17:23:29 +01:00
parent 6ce1bf71c1
commit 09d2fc850d
5 changed files with 61 additions and 15 deletions

View File

@@ -1230,11 +1230,16 @@ def _discover_plugins() -> dict[str, BasisPlugin]:
except Exception as exc: except Exception as exc:
xbmc.log(f"Plugin-Datei {file_path.name} konnte nicht geladen werden: {exc}", xbmc.LOGWARNING) xbmc.log(f"Plugin-Datei {file_path.name} konnte nicht geladen werden: {exc}", xbmc.LOGWARNING)
continue continue
preferred = getattr(module, "Plugin", None)
if inspect.isclass(preferred) and issubclass(preferred, BasisPlugin) and preferred is not BasisPlugin:
plugin_classes = [preferred]
else:
plugin_classes = [ plugin_classes = [
obj obj
for obj in module.__dict__.values() for obj in module.__dict__.values()
if inspect.isclass(obj) and issubclass(obj, BasisPlugin) and obj is not BasisPlugin 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: for cls in plugin_classes:
try: try:
instance = cls() instance = cls()
@@ -1245,7 +1250,21 @@ def _discover_plugins() -> dict[str, BasisPlugin]:
reason = getattr(instance, "unavailable_reason", "Nicht verfuegbar.") reason = getattr(instance, "unavailable_reason", "Nicht verfuegbar.")
xbmc.log(f"Plugin {cls.__name__} deaktiviert: {reason}", xbmc.LOGWARNING) xbmc.log(f"Plugin {cls.__name__} deaktiviert: {reason}", xbmc.LOGWARNING)
continue 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 _PLUGIN_CACHE = plugins
return plugins return plugins

View File

@@ -4,7 +4,7 @@
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import List, Optional, Set from typing import Any, Dict, List, Optional, Set, Tuple
class BasisPlugin(ABC): class BasisPlugin(ABC):
@@ -12,6 +12,7 @@ class BasisPlugin(ABC):
name: str name: str
version: str = "0.0.0" version: str = "0.0.0"
prefer_source_metadata: bool = False
@abstractmethod @abstractmethod
async def search_titles(self, query: str) -> List[str]: 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.""" """Optional: Liefert den Stream-Link fuer eine konkrete Folge."""
return None 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]: def resolve_stream_link(self, link: str) -> Optional[str]:
"""Optional: Folgt einem Stream-Link und liefert die finale URL.""" """Optional: Folgt einem Stream-Link und liefert die finale URL."""
return None return None

View File

@@ -10,7 +10,7 @@ Dieses Dokument beschreibt den Einstiegspunkt des Addons und die zentrale Steuer
- startet die Wiedergabe und verwaltet Playstate/Resume. - startet die Wiedergabe und verwaltet Playstate/Resume.
## Ablauf (high level) ## Ablauf (high level)
1. **PluginDiscovery**: Lädt alle `addon/plugins/*.py` (ohne `_`Prefix) und instanziiert Klassen, die von `BasisPlugin` erben. 1. **PluginDiscovery**: Lädt alle `addon/plugins/*.py` (ohne `_`Prefix). Bevorzugt `Plugin = <Klasse>`, sonst werden `BasisPlugin`Subklassen deterministisch instanziiert.
2. **Navigation**: Baut KodiListen (Serien/Staffeln/Episoden) auf Basis der PluginAntworten. 2. **Navigation**: Baut KodiListen (Serien/Staffeln/Episoden) auf Basis der PluginAntworten.
3. **Playback**: Holt StreamLinks aus dem Plugin und startet die Wiedergabe. 3. **Playback**: Holt StreamLinks aus dem Plugin und startet die Wiedergabe.
4. **Playstate**: Speichert ResumeDaten lokal (`playstate.json`) und setzt `playcount`/ResumeInfos. 4. **Playstate**: Speichert ResumeDaten lokal (`playstate.json`) und setzt `playcount`/ResumeInfos.
@@ -35,6 +35,7 @@ Die genaue Aktion wird aus den QueryParametern gelesen und an das entsprechen
- **PluginLoader**: findet & instanziiert Plugins. - **PluginLoader**: findet & instanziiert Plugins.
- **UIHelper**: setzt ContentType, baut Verzeichnisseinträge. - **UIHelper**: setzt ContentType, baut Verzeichnisseinträge.
- **PlaystateHelper**: `_load_playstate`, `_save_playstate`, `_apply_playstate_to_info`. - **PlaystateHelper**: `_load_playstate`, `_save_playstate`, `_apply_playstate_to_info`.
- **MetadataMerge**: PluginMetadaten können TMDB übersteuern, TMDB dient als Fallback.
## Fehlerbehandlung ## Fehlerbehandlung
- PluginImportfehler werden isoliert behandelt, damit das Addon nicht komplett ausfällt. - PluginImportfehler werden isoliert behandelt, damit das Addon nicht komplett ausfällt.

View File

@@ -6,6 +6,7 @@ Diese Doku beschreibt, wie Plugins im ViewITAddon aufgebaut sind und wie neue
- Jedes Plugin ist eine einzelne Datei unter `addon/plugins/`. - Jedes Plugin ist eine einzelne Datei unter `addon/plugins/`.
- Dateinamen **ohne** `_`Prefix werden automatisch geladen. - Dateinamen **ohne** `_`Prefix werden automatisch geladen.
- Jede Datei enthält eine Klasse, die von `BasisPlugin` erbt. - Jede Datei enthält eine Klasse, die von `BasisPlugin` erbt.
- Optional: `Plugin = <Klasse>` als expliziter Einstiegspunkt (bevorzugt vom Loader).
## PflichtMethoden (BasisPlugin) ## PflichtMethoden (BasisPlugin)
Jedes Plugin muss diese Methoden implementieren: Jedes Plugin muss diese Methoden implementieren:
@@ -22,6 +23,7 @@ Wesentliche Rückgaben an die Hauptlogik:
- `episodes_for(...)` → Liste von Episoden-Labels - `episodes_for(...)` → Liste von Episoden-Labels
- `stream_link_for(...)` → Hoster-/Player-Link (nicht zwingend finale Media-URL) - `stream_link_for(...)` → Hoster-/Player-Link (nicht zwingend finale Media-URL)
- `resolve_stream_link(...)` → finale/spielbare URL nach Redirect/Resolver - `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 `available_hosters_for(...)` → auswählbare Hoster-Namen im Dialog
- Optional `series_url_for_title(...)` → stabile Detail-URL pro Titel für Folgeaufrufe - Optional `series_url_for_title(...)` → stabile Detail-URL pro Titel für Folgeaufrufe
- Optional `remember_series_url(...)` → Übernahme einer bereits bekannten Detail-URL - 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()` - `popular_series``popular_series()`
- `genres``genres()` + `titles_for_genre(genre)` - `genres``genres()` + `titles_for_genre(genre)`
- `latest_episodes``latest_episodes(page=1)` - `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 ## Empfohlene Struktur
- Konstanten für URLs/Endpoints (BASE_URL, Pfade, Templates) - Konstanten für URLs/Endpoints (BASE_URL, Pfade, Templates)
@@ -60,6 +65,7 @@ Standard: `*_base_url` (Domain / BASE_URL)
- `einschalten_base_url` - `einschalten_base_url`
- `topstream_base_url` - `topstream_base_url`
- `filmpalast_base_url` - `filmpalast_base_url`
- `doku_streams_base_url`
## Playback ## Playback
- `stream_link_for(...)` implementieren (liefert bevorzugten Hoster-Link). - `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. 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. 3. **Auswahl Hoster**: Hoster-Namen aus der Detailseite extrahieren und anbieten.
4. **Playback**: Hoster-Link liefern, danach konsistent über `resolve_stream_link` finalisieren. 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 ## Debugging
Global gesteuert über Settings: Global gesteuert über Settings:

View File

@@ -17,6 +17,7 @@ Weitere Details:
- `einschalten_plugin.py` Einschalten - `einschalten_plugin.py` Einschalten
- `aniworld_plugin.py` Aniworld - `aniworld_plugin.py` Aniworld
- `filmpalast_plugin.py` Filmpalast - `filmpalast_plugin.py` Filmpalast
- `dokustreams_plugin.py` Doku-Streams
- `_template_plugin.py` Vorlage für neue Plugins - `_template_plugin.py` Vorlage für neue Plugins
### Plugin-Discovery (Ladeprozess) ### Plugin-Discovery (Ladeprozess)
@@ -26,8 +27,9 @@ Der Loader in `addon/default.py`:
1. Sucht alle `*.py` in `addon/plugins/` 1. Sucht alle `*.py` in `addon/plugins/`
2. Überspringt Dateien, die mit `_` beginnen 2. Überspringt Dateien, die mit `_` beginnen
3. Lädt Module dynamisch 3. Lädt Module dynamisch
4. Instanziert Klassen, die von `BasisPlugin` erben 4. Nutzt `Plugin = <Klasse>` als bevorzugten Einstiegspunkt (falls vorhanden)
5. Ignoriert Plugins mit `is_available = False` 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. 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]` - `async search_titles(query: str) -> list[str]`
- `seasons_for(title: str) -> list[str]` - `seasons_for(title: str) -> list[str]`
- `episodes_for(title: str, season: str) -> list[str]` - `episodes_for(title: str, season: str) -> list[str]`
- optional `metadata_for(title: str) -> (info_labels, art, cast)`
### Optionale Features (Capabilities) ### Optionale Features (Capabilities)
Plugins können zusätzliche Features anbieten: Plugins können zusätzliche Features anbieten:
- `capabilities() -> set[str]` - `capabilities() -> set[str]`
- `popular_series`: liefert beliebte Serien - `popular_series`: liefert beliebte Serien
- `genres`: Genre-Liste verfügbar - `genres`: Genre-Liste verfügbar
- `latest_episodes`: neue Episoden 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]` - `popular_series() -> list[str]`
- `genres() -> list[str]` - `genres() -> list[str]`
- `titles_for_genre(genre: str) -> list[str]` - `titles_for_genre(genre: str) -> list[str]`
- `latest_episodes(page: int = 1) -> list[LatestEpisode]` (wenn angeboten) - `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. 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 - `search_titles()` mit Provider-Suche
- `seasons_for()` und `episodes_for()` mit HTML-Parsing - `seasons_for()` und `episodes_for()` mit HTML-Parsing
- `stream_link_for()` optional für direkte Playback-Links - `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 - Optional: `available_hosters_for()` oder Provider-spezifische Helfer
Als Startpunkt dient `addon/plugins/_template_plugin.py`. 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 - Keine Netzwerkzugriffe im Import-Top-Level
- Netzwerkzugriffe nur in Methoden (z.B. `search_titles`) - Netzwerkzugriffe nur in Methoden (z.B. `search_titles`)
- Fehler sauber abfangen und verständliche Fehlermeldungen liefern - 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 - Provider-spezifische Parser in Helper-Funktionen kapseln
- Reproduzierbare Reihenfolge: `Plugin`-Alias nutzen oder Klassenname eindeutig halten
### Debugging & Logs ### Debugging & Logs