Files
ViewIT/addon/plugins/_template_plugin.py
itdrui.de 7b60b00c8b dev: umfangreiches Refactoring, Trakt-Integration und Code-Review-Fixes (0.1.69-dev)
Core & Architektur:
- Neues Verzeichnis addon/core/ mit router.py, trakt.py, metadata.py,
  gui.py, playstate.py, plugin_manager.py, updater.py
- Tests-Verzeichnis hinzugefügt (24 Tests, pytest + Coverage)

Trakt-Integration:
- OAuth Device Flow, Scrobbling, Watchlist, History, Calendar
- Upcoming Episodes, Weiterschauen (Continue Watching)
- Watched-Status in Episodenlisten
- _trakt_find_in_plugins() mit 5-Min-Cache

Serienstream-Suche:
- API-Ergebnisse werden immer mit Katalog-Cache ergänzt (serverseitiges 10-Treffer-Limit)
- Katalog-Cache wird beim Addon-Start im Daemon-Thread vorgewärmt
- Notification nach Cache-Load via xbmc.executebuiltin() (thread-sicher)

Bugfixes (Code-Review):
- Race Condition auf _TRAKT_WATCHED_CACHE: _TRAKT_WATCHED_CACHE_LOCK hinzugefügt
- GUI-Dialog aus Daemon-Thread: xbmcgui -> xbmc.executebuiltin()
- ValueError in Trakt-Watchlist-Routen abgesichert
- Token expires_at==0 Check korrigiert
- get_setting_bool() Kontrollfluss in gui.py bereinigt
- topstreamfilm_plugin: try-finally um xbmcvfs.File.close()

Cleanup:
- default.py.bak und refactor_router.py entfernt
- .gitignore: /tests/ Eintrag entfernt
- Type-Hints vereinheitlicht (Dict/List/Tuple -> dict/list/tuple)
2026-03-01 18:39:05 +01:00

209 lines
7.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""Template fuer ein neues ViewIt-Plugin.
Diese Datei wird NICHT automatisch geladen (Dateiname beginnt mit `_`).
Vorgehen fuer ein neues Plugin:
1. Datei kopieren/umbenennen (ohne fuehrenden Unterstrich), z.B. `my_site_plugin.py`
2. `name`, `ADDON_ID`, `BASE_URL` und Header anpassen
3. `search_titles`, `seasons_for`, `episodes_for` gemaess Zielseite implementieren
4. Optional weitere Methoden implementieren capabilities deklarieren und Methoden ueberschreiben:
- `popular_series()` + capability 'popular_series'
- `latest_titles(page)` + capability 'latest_titles'
- `genres()` + `titles_for_genre(genre)` + `titles_for_genre_page(genre, page)`
- `alpha_index()` + `titles_for_alpha_page(letter, page)`
- `years_available()` + `titles_for_year(year, page)` + capability 'year_filter'
- `countries_available()` + `titles_for_country(country, page)` + capability 'country_filter'
- `collections()` + `titles_for_collection(collection, page)` + capability 'collections'
- `tags()` + `titles_for_tag(tag, page)` + capability 'tags'
- `random_title()` + capability 'random'
- `stream_link_for(...)`, `resolve_stream_link(link)`, `available_hosters_for(...)`
- `metadata_for(title)` fuer eigene Metadaten
Siehe `docs/PLUGIN_DEVELOPMENT.md` und bestehende Plugins.
"""
from __future__ import annotations
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any, Callable, List, Optional
try: # pragma: no cover - optional dependency
import requests
from bs4 import BeautifulSoup # type: ignore[import-not-found]
except ImportError as exc: # pragma: no cover - optional dependency
requests = None
BeautifulSoup = None
REQUESTS_AVAILABLE = False
REQUESTS_IMPORT_ERROR = exc
else:
REQUESTS_AVAILABLE = True
REQUESTS_IMPORT_ERROR = None
try: # pragma: no cover - optional Kodi helpers
import xbmcaddon # type: ignore[import-not-found]
except ImportError: # pragma: no cover - allow running outside Kodi
xbmcaddon = None
from plugin_interface import BasisPlugin
if TYPE_CHECKING: # pragma: no cover
from requests import Session as RequestsSession
from bs4 import BeautifulSoup as BeautifulSoupT # type: ignore[import-not-found]
else: # pragma: no cover
RequestsSession = Any
BeautifulSoupT = Any
ADDON_ID = "plugin.video.viewit"
BASE_URL = "https://example.com"
DEFAULT_TIMEOUT = 20
HEADERS = {
"User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
"Connection": "keep-alive",
}
ProgressCallback = Optional[Callable[[str, Optional[int]], Any]]
@dataclass(frozen=True)
class TitleHit:
"""Ein einfacher Suchtreffer mit Titel und Detail-URL."""
title: str
url: str
class TemplatePlugin(BasisPlugin):
"""Vorlage fuer eine HTML-basierte Streamingseiten-Integration.
Dieses Template zeigt nur die MINIMALE, aber reale Schnittstelle:
Pflicht:
- `async search_titles(query, progress_callback=None) -> list[str]`
- `seasons_for(title) -> list[str]`
- `episodes_for(title, season) -> list[str]`
Empfohlen (optional, je nach Use-Case):
- `capabilities()` mit z.B. `popular_series`, `genres`, `latest_episodes`
- `popular_series()`, `titles_for_genre()`, `titles_for_genre_page()`
- `stream_link_for(...)` und/oder `stream_link_for_url(...)`
- `resolve_stream_link(link)` fuer Hosters/Redirects
- `metadata_for(title)` fuer eigene Metadaten (siehe bestehende Plugins)
"""
name = "Template"
def __init__(self) -> None:
self._session: RequestsSession | None = None
@property
def is_available(self) -> bool:
"""Signalisiert dem Router, ob das Plugin nutzbar ist (z.B. Abhaengigkeiten vorhanden)."""
return REQUESTS_AVAILABLE
@property
def unavailable_reason(self) -> str:
"""Optionaler Grund, warum `is_available` false ist (z.B. fehlende Pakete)."""
if REQUESTS_AVAILABLE:
return ""
return f"requests/bs4 nicht verfuegbar: {REQUESTS_IMPORT_ERROR}"
def _get_session(self) -> RequestsSession:
"""Gibt eine vorkonfigurierte `requests.Session` zurueck.
In echten Plugins kann hier auch `http_session_pool.get_requests_session(...)`
genutzt werden, wenn mehrere Module sich Sessions teilen sollen.
"""
if requests is None:
raise RuntimeError(self.unavailable_reason)
if self._session is None:
session = requests.Session()
session.headers.update(HEADERS)
self._session = session
return self._session
async def search_titles(
self,
query: str,
progress_callback: ProgressCallback = None,
) -> List[str]:
"""Sucht Titel auf der Zielseite und liefert eine Liste an Titel-Strings.
Best Practices:
- Nur passende Titel liefern (wortbasiert, keine Zufallstreffer).
- `progress_callback(message, percent)` sparsam nutzen, um lange Suchen anzuzeigen.
- HTTP-Requests robust kapseln (Timeouts, Fehlerbehandlung, optionales Logging).
"""
_ = (query, progress_callback)
return []
def seasons_for(self, title: str) -> List[str]:
"""Liefert alle Staffeln fuer einen Titel, z.B. `['Staffel 1', 'Staffel 2']`.
Fuer reine Film-Provider kann stattdessen z.B. `['Film']` zurueckgegeben werden
(siehe \"Film Provider Standard\" in `docs/PLUGIN_DEVELOPMENT.md`).
"""
_ = title
return []
def episodes_for(self, title: str, season: str) -> List[str]:
"""Liefert Episoden-Labels fuer einen Titel und eine Staffel.
Beispiele:
- `['Episode 1', 'Episode 2']`
- `['Episode 1: Pilot', 'Episode 2: Finale']`
"""
_ = (title, season)
return []
def capabilities(self) -> set[str]:
"""Optional: Deklariert die Faehigkeiten dieses Plugins.
Bekannte Werte (aus plugin_interface.py):
- 'popular_series' Plugin hat beliebte Serien/Filme
- 'latest_titles' Plugin hat neu hinzugefuegte Titel
- 'year_filter' Plugin unterstuetzt Jahr-Filter
- 'country_filter' Plugin unterstuetzt Land-Filter
- 'collections' Plugin hat Sammlungen/Filmreihen
- 'tags' Plugin hat Tag/Schlagwort-Suche
- 'random' Plugin kann einen zufaelligen Titel liefern
- 'genres' Plugin hat Genre-Browser
- 'alpha' Plugin hat A-Z-Index
- 'latest_episodes' Plugin liefert neue Episoden
"""
return set()
def popular_series(self) -> List[str]:
"""Optional: Liste beliebter Titel (wenn `popular_series` in `capabilities()` gesetzt ist)."""
return []
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
"""Optional: Embed-/Hoster-Link fuer eine Episode.
Der Router ruft diese Methode nur auf, wenn sie existiert. Der Rueckgabewert
ist entweder ein finaler Stream-Link oder ein Hoster-/Embed-Link, der spaeter
ueber `resolve_stream_link` oder ResolveURL weiter aufgeloest werden kann.
"""
_ = (title, season, episode)
return None
def resolve_stream_link(self, link: str) -> Optional[str]:
"""Optional: Redirect-/Mirror-Aufloesung fuer Hoster-Links.
Falls nicht ueberschrieben, kann der Router (oder ResolveURL) den Link
direkt verwenden. Plugins koennen hier z.B. HTTP-Redirects verfolgen.
"""
return link