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

@@ -38,7 +38,7 @@ HEADERS = {
"Connection": "keep-alive",
}
_URL_SEARCH = BASE_URL + "/?s={query}"
_URL_SEARCH = BASE_URL + "/?do=search&subaction=search&story={query}"
_URL_NEW = BASE_URL + "/kinofilme-online/"
_URL_SERIES = BASE_URL + "/serienstream-deutsch/"
@@ -68,7 +68,7 @@ GENRE_SLUGS: dict[str, str] = {
}
# Hoster die übersprungen werden (kein Stream / nur Trailer)
_SKIP_LINK_KEYWORDS = ("youtube.com", "youtu.be")
_SKIP_LINK_KEYWORDS = ("youtube.com", "youtu.be", "hdfilme-tv.cc")
ProgressCallback = Optional[Callable[[str, Optional[int]], Any]]
@@ -122,6 +122,7 @@ class HdfilmePlugin(BasisPlugin):
self._is_series: dict[str, bool] = {}
self._title_meta: dict[str, tuple[str, str]] = {} # title → (plot, poster)
self._episode_cache: dict[str, list[str]] = {} # detail_url → episode labels
self._preferred_hosters: list[str] = []
# ------------------------------------------------------------------
# Verfügbarkeit
@@ -182,40 +183,64 @@ class HdfilmePlugin(BasisPlugin):
titles.append(title)
return titles
def _ensure_detail_url(self, title: str) -> str:
"""Gibt die Detail-URL für einen Titel zurück.
Sucht zuerst im Cache, dann live über die Suchfunktion.
"""
url = self._title_to_url.get(title, "")
if url:
return url
# Fallback: Live-Suche (nötig wenn Plugin-Instanz neu, Cache leer)
search_url = _URL_SEARCH.format(query=quote_plus(title.strip()))
soup = _get_soup(search_url)
if soup:
self._parse_entries(soup)
url = self._title_to_url.get(title, "")
return url
def _get_detail_soup(self, title: str) -> Any:
"""Lädt die Detailseite eines Titels."""
url = self._title_to_url.get(title, "")
url = self._ensure_detail_url(title)
if not url:
return None
return _get_soup(url)
def _extract_hoster_links(self, soup: Any, episode_id: str = "") -> list[str]:
def _extract_hoster_links(self, soup: Any, episode_id: str = "") -> dict[str, str]:
"""Extrahiert Hoster-Links aus einer Detailseite.
Gibt dict {Hoster-Name → URL} zurück.
episode_id: wenn gesetzt, nur Links aus dem `<li id="{episode_id}">` Block.
"""
if soup is None:
return []
links: list[str] = []
return {}
hosters: dict[str, str] = {}
if episode_id:
# Serien-Episode: Links aus dem spezifischen Episode-Container
container = soup.select_one(f"li#{episode_id}")
if container is None:
return []
return {}
candidates = container.select("a[data-link]")
else:
# Film: Links aus .mirrors
candidates = soup.select(".mirrors [data-link]")
seen_names: set[str] = set()
for el in candidates:
href = _absolute_url((el.get("data-link") or "").strip())
if not href:
continue
if any(kw in href for kw in _SKIP_LINK_KEYWORDS):
continue
links.append(href)
return links
name = el.get_text(strip=True) or "Hoster"
# Eindeutiger Name bei Duplikaten
base_name = name
i = 2
while name in seen_names:
name = f"{base_name} {i}"
i += 1
seen_names.add(name)
hosters[name] = href
return hosters
def _staffel_nr(self, season: str) -> int:
"""Extrahiert die Staffelnummer aus einem Label wie 'Staffel 2'."""
@@ -270,7 +295,7 @@ class HdfilmePlugin(BasisPlugin):
if season == "Film":
return [title]
detail_url = self._title_to_url.get(title, "")
detail_url = self._ensure_detail_url(title)
cached = self._episode_cache.get(detail_url)
if cached is not None:
return cached
@@ -304,27 +329,40 @@ class HdfilmePlugin(BasisPlugin):
self._episode_cache[detail_url] = result
return result
def _hosters_for(self, title: str, season: str, episode: str) -> dict[str, str]:
"""Gibt alle verfügbaren Hoster {Name → URL} für Titel/Staffel/Episode zurück."""
soup = self._get_detail_soup(title)
if soup is None:
return {}
if season == "Film" or not self._is_series.get(title, False):
return self._extract_hoster_links(soup)
staffel_nr = self._staffel_nr(season)
ep_idx = self._ep_index(episode)
episode_id = f"serie-{staffel_nr}_{ep_idx}"
return self._extract_hoster_links(soup, episode_id)
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()
season = (season or "").strip()
if not title:
return None
soup = self._get_detail_soup(title)
if soup is None:
hosters = self._hosters_for(title, season, episode)
if not hosters:
return None
if season == "Film" or not self._is_series.get(title, False):
# Film: .mirrors [data-link]
links = self._extract_hoster_links(soup)
else:
# Serie: Episode-Container
staffel_nr = self._staffel_nr(season)
ep_idx = self._ep_index(episode)
episode_id = f"serie-{staffel_nr}_{ep_idx}"
links = self._extract_hoster_links(soup, episode_id)
return links[0] if links else 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()
@@ -390,7 +428,12 @@ class HdfilmePlugin(BasisPlugin):
# Browsing
# ------------------------------------------------------------------
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles(self) -> List[str]:
if not REQUESTS_AVAILABLE:
return []
return self._parse_entries(_get_soup(_URL_NEW))
def new_titles_page(self, page: int = 1) -> List[str]:
if not REQUESTS_AVAILABLE:
return []
page = max(1, int(page or 1))
@@ -417,4 +460,4 @@ class HdfilmePlugin(BasisPlugin):
return self._parse_entries(_get_soup(url))
def capabilities(self) -> set[str]:
return {"latest_titles", "popular_series", "genres"}
return {"new_titles", "popular_series", "genres"}