dev: bump to 0.1.72-dev – Autoplay-Setting, Moflix Hoster-Dialog, Update-Hinweis im Hauptmenue

This commit is contained in:
2026-03-06 21:05:53 +01:00
parent 957a5a1aea
commit 6e7b4c3d39
13 changed files with 473 additions and 205 deletions

View File

@@ -93,6 +93,8 @@ class KKistePlugin(BasisPlugin):
self._is_series: dict[str, bool] = {}
# title → Staffelnummer (aus "Staffel N" extrahiert)
self._season_nr: dict[str, int] = {}
# bevorzugte Hoster für Hoster-Dialog
self._preferred_hosters: list[str] = []
# ------------------------------------------------------------------
# Verfügbarkeit
@@ -165,6 +167,24 @@ class KKistePlugin(BasisPlugin):
self._title_meta[title] = (plot, poster, fanart)
return title
def _ensure_watch_url(self, title: str) -> str:
"""Gibt die Watch-URL zurück lädt bei leerem Cache alle Titel nach."""
url = self._title_to_watch_url.get(title, "")
if url:
return url
# Fallback: alle Titel laden und exact-match suchen
search_url = _URL_SEARCH.format(lang=_LANG)
data = self._get_json(search_url)
if isinstance(data, dict):
q_lower = title.lower()
for movie in (data.get("movies") or []):
if isinstance(movie, dict):
raw = str(movie.get("title") or "").strip()
if raw.lower() == q_lower:
self._cache_entry(movie)
return self._title_to_watch_url.get(title, "")
return ""
def _browse(self, content_type: str, order: str = "Trending") -> List[str]:
url = _URL_BROWSE.format(lang=_LANG, type=content_type, order=order, page=1)
data = self._get_json(url)
@@ -175,6 +195,55 @@ class KKistePlugin(BasisPlugin):
if isinstance(movie, dict) and (t := self._cache_entry(movie))
]
def _hosters_for(self, title: str, season: str, episode: str) -> dict[str, str]:
"""Gibt {Hoster-Name → URL} für Titel/Staffel/Episode zurück."""
watch_url = self._ensure_watch_url(title)
if not watch_url:
return {}
data = self._get_json(watch_url)
if not isinstance(data, dict):
return {}
streams = data.get("streams") or []
hosters: dict[str, str] = {}
seen: set[str] = set()
# Film vs Serie: relevante Streams filtern
if season == "Film":
target_streams = [s for s in streams if isinstance(s, dict)]
else:
m = re.search(r"\d+", episode or "")
ep_nr = int(m.group()) if m else None
if ep_nr is None:
return {}
target_streams = [
s for s in streams
if isinstance(s, dict) and s.get("e") == ep_nr
]
for stream in target_streams:
src = str(stream.get("stream") or "").strip()
if not src:
continue
# Hoster-Name aus der Stream-URL extrahieren (nicht aus "source" das ist die Aggregator-Quelle)
try:
from urllib.parse import urlparse
host = urlparse(src).hostname or "Hoster"
# Domain-Prefix entfernen (www.)
if host.startswith("www."):
host = host[4:]
except Exception:
host = "Hoster"
name = host
base_name = name
i = 2
while name in seen:
name = f"{base_name} {i}"
i += 1
seen.add(name)
hosters[name] = src
return hosters
# ------------------------------------------------------------------
# Pflicht-Methoden
# ------------------------------------------------------------------
@@ -211,10 +280,14 @@ class KKistePlugin(BasisPlugin):
return []
is_series = self._is_series.get(title)
if is_series is None:
# Cache leer (neue Instanz) nachfüllen
self._ensure_watch_url(title)
is_series = self._is_series.get(title)
if is_series:
season_nr = self._season_nr.get(title, 1)
return [f"Staffel {season_nr}"]
# Film (oder unbekannt → Film-Fallback)
return ["Film"]
def episodes_for(self, title: str, season: str) -> List[str]:
@@ -222,12 +295,11 @@ class KKistePlugin(BasisPlugin):
if not title:
return []
# Film
if season == "Film":
return [title]
# Serie: Episodenliste aus /data/watch/ laden
watch_url = self._title_to_watch_url.get(title, "")
watch_url = self._ensure_watch_url(title)
if not watch_url:
return []
@@ -247,7 +319,6 @@ class KKistePlugin(BasisPlugin):
pass
if not episode_nrs:
# Keine Episoden-Nummern → als Film behandeln
return [title]
return [f"Episode {nr}" for nr in sorted(episode_nrs)]
@@ -256,48 +327,25 @@ class KKistePlugin(BasisPlugin):
# Stream
# ------------------------------------------------------------------
def available_hosters_for(self, title: str, season: str, episode: str) -> List[str]:
return list(self._hosters_for(title, season, episode).keys())
def set_preferred_hosters(self, hosters: List[str]) -> None:
self._preferred_hosters = [h for h in hosters if h]
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
title = (title or "").strip()
watch_url = self._title_to_watch_url.get(title, "")
if not watch_url:
hosters = self._hosters_for(title, season, episode)
if not hosters:
return None
data = self._get_json(watch_url)
if not isinstance(data, dict):
return None
streams = data.get("streams") or []
if season == "Film":
# Film: Stream ohne Episode-Nummer bevorzugen
for stream in streams:
if isinstance(stream, dict) and stream.get("e") is None:
src = str(stream.get("stream") or "").strip()
if src:
return src
# Fallback: irgendeinen Stream
for stream in streams:
if isinstance(stream, dict):
src = str(stream.get("stream") or "").strip()
if src:
return src
else:
# Serie: Episodennummer extrahieren und matchen
m = re.search(r"\d+", episode or "")
if not m:
return None
ep_nr = int(m.group())
for stream in streams:
if not isinstance(stream, dict):
continue
try:
if int(stream.get("e") or -1) == ep_nr:
src = str(stream.get("stream") or "").strip()
if src:
return src
except (ValueError, TypeError):
pass
return None
# Bevorzugten Hoster nutzen falls gesetzt
for preferred in self._preferred_hosters:
key = preferred.casefold()
for name, url in hosters.items():
if key in name.casefold() or key in url.casefold():
return url
# Fallback: erster Hoster
return next(iter(hosters.values()))
def resolve_stream_link(self, link: str) -> Optional[str]:
link = (link or "").strip()
@@ -341,12 +389,23 @@ class KKistePlugin(BasisPlugin):
# Browsing
# ------------------------------------------------------------------
def new_titles(self) -> List[str]:
return self._browse("movies", "new")
def new_titles_page(self, page: int = 1) -> List[str]:
page = max(1, int(page or 1))
url = _URL_BROWSE.format(lang=_LANG, type="movies", order="new", page=page)
data = self._get_json(url)
if not isinstance(data, dict):
return []
return [
t for movie in (data.get("movies") or [])
if isinstance(movie, dict) and (t := self._cache_entry(movie))
]
def popular_series(self) -> List[str]:
return self._browse("tvseries", "views")
def latest_titles(self, page: int = 1) -> List[str]:
return self._browse("movies", "new")
def genres(self) -> List[str]:
return sorted(GENRE_SLUGS.keys())
@@ -364,4 +423,4 @@ class KKistePlugin(BasisPlugin):
]
def capabilities(self) -> set[str]:
return {"popular_series", "latest_titles", "genres"}
return {"popular_series", "new_titles", "genres"}