diff --git a/addon/addon.xml b/addon/addon.xml index 479247e..7e3746e 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/plugins/serienstream_plugin.py b/addon/plugins/serienstream_plugin.py index d2f67f3..f34a51a 100644 --- a/addon/plugins/serienstream_plugin.py +++ b/addon/plugins/serienstream_plugin.py @@ -111,6 +111,57 @@ class SeasonInfo: episodes: List[EpisodeInfo] +def _extract_series_metadata(soup: BeautifulSoupT) -> Tuple[Dict[str, str], Dict[str, str]]: + info: Dict[str, str] = {} + art: Dict[str, str] = {} + if not soup: + return info, art + + title_tag = soup.select_one("h1") + title = (title_tag.get_text(" ", strip=True) if title_tag else "").strip() + if title: + info["title"] = title + + description = "" + desc_tag = soup.select_one(".series-description .description-text") + if desc_tag: + description = (desc_tag.get_text(" ", strip=True) or "").strip() + if not description: + meta_desc = soup.select_one("meta[property='og:description'], meta[name='description']") + if meta_desc: + description = (meta_desc.get("content") or "").strip() + if description: + info["plot"] = description + + poster = "" + poster_tag = soup.select_one( + ".show-cover-mobile img[data-src], .show-cover-mobile img[src], .col-3 img[data-src], .col-3 img[src]" + ) + if poster_tag: + poster = (poster_tag.get("data-src") or poster_tag.get("src") or "").strip() + if not poster: + for candidate in soup.select("img[data-src], img[src]"): + url = (candidate.get("data-src") or candidate.get("src") or "").strip() + if "/media/images/channel/" in url: + poster = url + break + if poster: + poster = _absolute_url(poster) + art["poster"] = poster + art["thumb"] = poster + + fanart = "" + fanart_tag = soup.select_one("meta[property='og:image']") + if fanart_tag: + fanart = (fanart_tag.get("content") or "").strip() + if fanart: + fanart = _absolute_url(fanart) + art["fanart"] = fanart + art["landscape"] = fanart + + return info, art + + def _get_base_url() -> str: base = get_setting_string(ADDON_ID, SETTING_BASE_URL, default=DEFAULT_BASE_URL).strip() if not base: @@ -805,6 +856,7 @@ class SerienstreamPlugin(BasisPlugin): self._hoster_cache: Dict[Tuple[str, str, str], List[str]] = {} self._latest_cache: Dict[int, List[LatestEpisode]] = {} self._latest_hoster_cache: Dict[str, List[str]] = {} + self._series_metadata_cache: Dict[str, Tuple[Dict[str, str], Dict[str, str]]] = {} self.is_available = True self.unavailable_reason: Optional[str] = None if not self._requests_available: # pragma: no cover - optional dependency @@ -851,12 +903,30 @@ class SerienstreamPlugin(BasisPlugin): cache_key = title.casefold() if self._title_url_cache.get(cache_key) != url: self._title_url_cache[cache_key] = url - self._save_title_url_cache() + self._save_title_url_cache() + if url: return current = self._series_results.get(title) if current is None: self._series_results[title] = SeriesResult(title=title, description=description, url="") + @staticmethod + def _metadata_cache_key(title: str) -> str: + return (title or "").strip().casefold() + + def _series_for_title(self, title: str) -> Optional[SeriesResult]: + direct = self._series_results.get(title) + if direct and direct.url: + return direct + lookup_key = (title or "").strip().casefold() + for item in self._series_results.values(): + if item.title.casefold().strip() == lookup_key and item.url: + return item + cached_url = self._title_url_cache.get(lookup_key, "") + if cached_url: + return SeriesResult(title=title, description="", url=cached_url) + return None + @staticmethod def _season_links_cache_name(series_url: str) -> str: digest = hashlib.sha1((series_url or "").encode("utf-8")).hexdigest()[:20] @@ -1274,7 +1344,28 @@ class SerienstreamPlugin(BasisPlugin): self._season_links_cache[title] = list(session_links) return list(session_links) try: - seasons = scrape_series_detail(series.url, load_episodes=False) + series_soup = _get_soup(series.url, session=get_requests_session("serienstream", headers=HEADERS)) + info_labels, art = _extract_series_metadata(series_soup) + if series.description and "plot" not in info_labels: + info_labels["plot"] = series.description + cache_key = self._metadata_cache_key(title) + if info_labels or art: + self._series_metadata_cache[cache_key] = (info_labels, art) + + base_series_url = _series_root_url(_extract_canonical_url(series_soup, series.url)) + season_links = _extract_season_links(series_soup) + season_count = _extract_number_of_seasons(series_soup) + if season_count and (not season_links or len(season_links) < season_count): + existing = {number for number, _ in season_links} + for number in range(1, season_count + 1): + if number in existing: + continue + season_url = f"{base_series_url}/staffel-{number}" + _log_parsed_url(season_url) + season_links.append((number, season_url)) + season_links.sort(key=lambda item: item[0]) + seasons = [SeasonInfo(number=number, url=url, episodes=[]) for number, url in season_links] + seasons.sort(key=lambda s: s.number) except Exception as exc: # pragma: no cover - defensive logging raise RuntimeError(f"Serienstream-Staffeln konnten nicht geladen werden: {exc}") from exc self._season_links_cache[title] = list(seasons) @@ -1288,6 +1379,41 @@ class SerienstreamPlugin(BasisPlugin): return self._remember_series_result(title, series_url) + def metadata_for(self, title: str) -> Tuple[Dict[str, str], Dict[str, str], Optional[List[Any]]]: + title = (title or "").strip() + if not title or not self._requests_available: + return {}, {}, None + + cache_key = self._metadata_cache_key(title) + cached = self._series_metadata_cache.get(cache_key) + if cached is not None: + info, art = cached + return dict(info), dict(art), None + + series = self._series_for_title(title) + if series is None or not series.url: + info = {"title": title} + self._series_metadata_cache[cache_key] = (dict(info), {}) + return info, {}, None + + info: Dict[str, str] = {"title": title} + art: Dict[str, str] = {} + if series.description: + info["plot"] = series.description + + try: + soup = _get_soup(series.url, session=get_requests_session("serienstream", headers=HEADERS)) + parsed_info, parsed_art = _extract_series_metadata(soup) + if parsed_info: + info.update(parsed_info) + if parsed_art: + art.update(parsed_art) + except Exception: + pass + + self._series_metadata_cache[cache_key] = (dict(info), dict(art)) + return info, art, None + def series_url_for_title(self, title: str) -> str: title = (title or "").strip() if not title: