dev: bump to 0.1.71-dev – neue Plugins (Moflix, KKiste, HDFilme, Netzkino), SerienStream A-Z, VidHide-Fix
This commit is contained in:
251
addon/plugins/netzkino_plugin.py
Normal file
251
addon/plugins/netzkino_plugin.py
Normal file
@@ -0,0 +1,251 @@
|
||||
"""NetzkKino Plugin für ViewIT.
|
||||
|
||||
Nutzt die öffentliche JSON-API von Netzkino.
|
||||
Nur Filme, keine Serien. Direkte MP4-Streams – kein ResolveURL nötig.
|
||||
Legal und kostenlos.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Callable, List, Optional
|
||||
|
||||
try: # pragma: no cover
|
||||
import requests
|
||||
except ImportError as exc: # pragma: no cover
|
||||
requests = None
|
||||
REQUESTS_AVAILABLE = False
|
||||
REQUESTS_IMPORT_ERROR = exc
|
||||
else:
|
||||
REQUESTS_AVAILABLE = True
|
||||
REQUESTS_IMPORT_ERROR = None
|
||||
|
||||
from plugin_interface import BasisPlugin
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
DEFAULT_TIMEOUT = 20
|
||||
|
||||
HEADERS = {
|
||||
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
|
||||
"Accept": "application/json, text/plain, */*",
|
||||
"Accept-Language": "de-DE,de;q=0.9,en;q=0.8",
|
||||
}
|
||||
|
||||
_API_BASE = "https://api.netzkino.de.simplecache.net/capi-2.0a"
|
||||
_STREAM_BASE = "https://pmd.netzkino-seite.netzkino.de"
|
||||
|
||||
_URL_SEARCH = _API_BASE + "/search?q={query}&d=www&l=de-DE"
|
||||
_URL_CATEGORY = _API_BASE + "/categories/{slug}.json?d=www&l=de-DE"
|
||||
|
||||
# Slug → Anzeigename
|
||||
CATEGORIES: dict[str, str] = {
|
||||
"highlights": "Highlights",
|
||||
"neue-filme": "Neue Filme",
|
||||
"alle-filme": "Alle Filme",
|
||||
"action": "Action",
|
||||
"animation": "Animation",
|
||||
"dokumentarfilm": "Dokumentation",
|
||||
"drama": "Drama",
|
||||
"fantasy": "Fantasy",
|
||||
"horror": "Horror",
|
||||
"komodie": "Komödie",
|
||||
"krimi-thriller": "Krimi & Thriller",
|
||||
"romantik": "Romantik",
|
||||
"sci-fi": "Science-Fiction",
|
||||
}
|
||||
|
||||
ProgressCallback = Optional[Callable[[str, Optional[int]], Any]]
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Plugin-Klasse
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class NetzkinoPlugin(BasisPlugin):
|
||||
"""NetzkKino Integration für ViewIT.
|
||||
|
||||
Alle Titel sind Filme (keine Serien). Streams sind direkte MP4-URLs.
|
||||
"""
|
||||
|
||||
name = "NetzkKino"
|
||||
|
||||
def __init__(self) -> None:
|
||||
# title → direkte MP4-URL
|
||||
self._title_to_stream: dict[str, str] = {}
|
||||
# title → (plot, poster, fanart)
|
||||
self._title_meta: dict[str, tuple[str, str, str]] = {}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Verfügbarkeit
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
@property
|
||||
def is_available(self) -> bool:
|
||||
return REQUESTS_AVAILABLE
|
||||
|
||||
@property
|
||||
def unavailable_reason(self) -> str:
|
||||
if REQUESTS_AVAILABLE:
|
||||
return ""
|
||||
return f"requests nicht verfügbar: {REQUESTS_IMPORT_ERROR}"
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# HTTP
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _get_session(self): # type: ignore[return]
|
||||
from http_session_pool import get_requests_session
|
||||
return get_requests_session("netzkino", headers=HEADERS)
|
||||
|
||||
def _get_json(self, url: str) -> dict | list | None:
|
||||
session = self._get_session()
|
||||
response = None
|
||||
try:
|
||||
response = session.get(url, headers=HEADERS, timeout=DEFAULT_TIMEOUT)
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except Exception:
|
||||
return None
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Interne Hilfsmethoden
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _build_stream_url(self, streaming_id: str) -> str:
|
||||
return f"{_STREAM_BASE}/{streaming_id}.mp4"
|
||||
|
||||
def _cache_post(self, post: dict) -> str:
|
||||
"""Cached einen API-Post und gibt den Titel zurück ('' = überspringen)."""
|
||||
title = str(post.get("title") or "").strip()
|
||||
if not title:
|
||||
return ""
|
||||
|
||||
# Stream-URL aus custom_fields.Streaming[0]
|
||||
custom = post.get("custom_fields") or {}
|
||||
streaming_ids = custom.get("Streaming") or []
|
||||
if not streaming_ids or not streaming_ids[0]:
|
||||
return ""
|
||||
|
||||
stream_url = self._build_stream_url(str(streaming_ids[0]))
|
||||
self._title_to_stream[title] = stream_url
|
||||
|
||||
# Metadaten
|
||||
plot = str(post.get("content") or "").strip()
|
||||
# Poster: thumbnail
|
||||
poster = str(post.get("thumbnail") or "").strip()
|
||||
# Fanart: featured_img_all[0]
|
||||
fanart_list = custom.get("featured_img_all") or []
|
||||
fanart = str(fanart_list[0]).strip() if fanart_list and fanart_list[0] else ""
|
||||
|
||||
self._title_meta[title] = (plot, poster, fanart)
|
||||
return title
|
||||
|
||||
def _load_posts(self, url: str) -> List[str]:
|
||||
data = self._get_json(url)
|
||||
if not isinstance(data, dict):
|
||||
return []
|
||||
titles: list[str] = []
|
||||
for post in (data.get("posts") or []):
|
||||
if not isinstance(post, dict):
|
||||
continue
|
||||
t = self._cache_post(post)
|
||||
if t:
|
||||
titles.append(t)
|
||||
return titles
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Pflicht-Methoden
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
async def search_titles(
|
||||
self, query: str, progress_callback: ProgressCallback = None
|
||||
) -> List[str]:
|
||||
query = (query or "").strip()
|
||||
if not query or not REQUESTS_AVAILABLE:
|
||||
return []
|
||||
from urllib.parse import quote_plus
|
||||
url = _URL_SEARCH.format(query=quote_plus(query))
|
||||
return self._load_posts(url)
|
||||
|
||||
def seasons_for(self, title: str) -> List[str]:
|
||||
# NetzkKino hat ausschließlich Filme
|
||||
return ["Film"]
|
||||
|
||||
def episodes_for(self, title: str, season: str) -> List[str]:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return []
|
||||
# Nur eine Episode: der Film selbst
|
||||
return [title]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Stream
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
|
||||
title = (title or "").strip()
|
||||
return self._title_to_stream.get(title)
|
||||
|
||||
def resolve_stream_link(self, link: str) -> Optional[str]:
|
||||
# Direkte MP4-URL – keine Auflösung nötig
|
||||
link = (link or "").strip()
|
||||
return link if link else None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Metadaten
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def metadata_for(
|
||||
self, title: str
|
||||
) -> tuple[dict[str, str], dict[str, str], list | None]:
|
||||
title = (title or "").strip()
|
||||
if not title:
|
||||
return {}, {}, None
|
||||
|
||||
info: dict[str, str] = {"title": title}
|
||||
art: dict[str, str] = {}
|
||||
|
||||
cached = self._title_meta.get(title)
|
||||
if cached:
|
||||
plot, poster, fanart = cached
|
||||
if plot:
|
||||
info["plot"] = plot
|
||||
if poster:
|
||||
art["thumb"] = poster
|
||||
art["poster"] = poster
|
||||
if fanart:
|
||||
art["fanart"] = fanart
|
||||
art["landscape"] = fanart
|
||||
|
||||
return info, art, None
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Browsing
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def latest_titles(self, page: int = 1) -> List[str]:
|
||||
url = _URL_CATEGORY.format(slug="neue-filme")
|
||||
return self._load_posts(url)
|
||||
|
||||
def genres(self) -> List[str]:
|
||||
# Gibt die Anzeigenamen zurück (sortiert, Browsing-Kategorien)
|
||||
return sorted(CATEGORIES.values())
|
||||
|
||||
def titles_for_genre(self, genre: str) -> List[str]:
|
||||
# Slug aus Anzeigename rückauflösen
|
||||
slug = next((s for s, n in CATEGORIES.items() if n == genre), "")
|
||||
if not slug:
|
||||
return []
|
||||
url = _URL_CATEGORY.format(slug=slug)
|
||||
return self._load_posts(url)
|
||||
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"latest_titles", "genres"}
|
||||
Reference in New Issue
Block a user