"""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' - `new_titles()` + `new_titles_page(page)` + capability 'new_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 - 'new_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