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

@@ -218,6 +218,8 @@ class MoflixPlugin(BasisPlugin):
self._season_api_ids: dict[tuple[str, int], str] = {}
# (title, season_nr) → Liste der Episode-Labels
self._episode_labels: dict[tuple[str, int], list[str]] = {}
# bevorzugte Hoster für Hoster-Dialog
self._preferred_hosters: list[str] = []
# ------------------------------------------------------------------
# Verfügbarkeit
@@ -510,57 +512,46 @@ class MoflixPlugin(BasisPlugin):
# Stream
# ------------------------------------------------------------------
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
def _videos_for(self, title: str, season: str, episode: str) -> list[dict]:
"""Gibt die rohe videos[]-Liste für einen Titel/Staffel/Episode zurück."""
title = (title or "").strip()
season = (season or "").strip()
if season == "Film":
return self._stream_link_for_movie(title)
url = self._ensure_title_url(title)
if not url:
self._resolve_title(title)
url = self._ensure_title_url(title)
if not url:
return []
data = self._get_json(url)
if not isinstance(data, dict):
return []
return (data.get("title") or {}).get("videos") or []
season_nr = _extract_first_number(season)
episode_nr = _extract_first_number(episode)
if season_nr is None or episode_nr is None:
return None
return []
# Season-API-ID ermitteln (mit Cache-Miss-Fallback)
api_id = self._season_api_ids.get((title, season_nr), "")
if not api_id:
self.seasons_for(title)
api_id = self._season_api_ids.get((title, season_nr), "")
if not api_id:
return None
return []
# Episoden-Detail laden enthält videos[] mit src-URLs
url = _URL_EPISODE.format(id=api_id, s=season_nr, e=episode_nr)
data = self._get_json(url)
if not isinstance(data, dict):
return None
videos = (data.get("episode") or {}).get("videos") or []
return self._best_src_from_videos(videos)
return []
return (data.get("episode") or {}).get("videos") or []
def _stream_link_for_movie(self, title: str) -> Optional[str]:
"""Wählt den besten src-Link eines Films aus den API-Videos."""
url = self._ensure_title_url(title)
if not url:
self._resolve_title(title)
url = self._ensure_title_url(title)
if not url:
return None
data = self._get_json(url)
if not isinstance(data, dict):
return None
videos = (data.get("title") or {}).get("videos") or []
return self._best_src_from_videos(videos)
def _best_src_from_videos(self, videos: object) -> Optional[str]:
"""Wählt die beste src-URL aus einer videos[]-Liste.
Priorisiert bekannte auflösbare Hoster (vidara.to),
überspringt Domains die erfahrungsgemäß 403 liefern.
"""
preferred: list[str] = []
fallback: list[str] = []
for v in (videos if isinstance(videos, list) else []):
def _hosters_from_videos(self, videos: list) -> dict[str, str]:
"""Konvertiert videos[] zu {Hoster-Name → src-URL}, mit Skip/Prefer-Logik."""
hosters: dict[str, str] = {}
seen: set[str] = set()
for v in videos:
if not isinstance(v, dict):
continue
src = _safe_str(v.get("src"))
@@ -569,12 +560,44 @@ class MoflixPlugin(BasisPlugin):
domain = urlparse(src).netloc.lstrip("www.")
if domain in _VIDEO_SKIP_DOMAINS:
continue
name = _normalize_video_name(_safe_str(v.get("name")), src)
if not name:
name = domain
base_name = name
i = 2
while name in seen:
name = f"{base_name} {i}"
i += 1
seen.add(name)
hosters[name] = src
return hosters
def available_hosters_for(self, title: str, season: str, episode: str) -> List[str]:
videos = self._videos_for(title, season, episode)
return list(self._hosters_from_videos(videos).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]:
videos = self._videos_for(title, season, episode)
if not videos:
return None
hosters = self._hosters_from_videos(videos)
if not hosters:
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: Prefer-Domains zuerst, dann Rest
for url in hosters.values():
domain = urlparse(url).netloc.lstrip("www.")
if domain in _VIDEO_PREFER_DOMAINS:
preferred.append(src)
else:
fallback.append(src)
candidates = preferred + fallback
return candidates[0] if candidates else None
return url
return next(iter(hosters.values()))
def _resolve_vidara(self, filecode: str) -> Optional[str]:
"""Löst einen vidara.to-Filecode über die vidara-API auf → HLS-URL."""
@@ -727,7 +750,10 @@ class MoflixPlugin(BasisPlugin):
def popular_series(self) -> List[str]:
return self._titles_from_channel("series")
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles(self) -> List[str]:
return self._titles_from_channel("now-playing")
def new_titles_page(self, page: int = 1) -> List[str]:
return self._titles_from_channel("now-playing", page=page)
def genres(self) -> List[str]:
@@ -752,4 +778,4 @@ class MoflixPlugin(BasisPlugin):
return self._titles_from_channel(slug, page=page)
def capabilities(self) -> set[str]:
return {"popular_series", "latest_titles", "collections", "genres"}
return {"popular_series", "new_titles", "collections", "genres"}