From 3689aedd23dbfae628ae4c674f892d749a8ed932 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Sun, 1 Feb 2026 21:19:28 +0100 Subject: [PATCH] Speed up serienstream season and episode loading --- .gitignore | 5 ++ addon/plugins/serienstream_plugin.py | 86 ++++++++++++++++++---------- 2 files changed, 62 insertions(+), 29 deletions(-) diff --git a/.gitignore b/.gitignore index 4922d5e..516722c 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,8 @@ # Build outputs /dist/ + +# Local tests (not committed) +/tests/ +/.pytest_cache/ +/pytest.ini diff --git a/addon/plugins/serienstream_plugin.py b/addon/plugins/serienstream_plugin.py index 17a1401..f96cc67 100644 --- a/addon/plugins/serienstream_plugin.py +++ b/addon/plugins/serienstream_plugin.py @@ -644,17 +644,14 @@ def resolve_redirect(target_url: str) -> Optional[str]: def scrape_series_detail( series_identifier: str, max_seasons: Optional[int] = None, + *, + load_episodes: bool = True, ) -> List[SeasonInfo]: _ensure_requests() series_url = _series_root_url(_normalize_series_url(series_identifier)) _log_url(series_url, kind="SERIES") _notify_url(series_url) session = get_requests_session("serienstream", headers=HEADERS) - # Preflight ist optional; manche Umgebungen/Provider leiten die Startseite um. - try: - _get_soup(_get_base_url(), session=session) - except Exception: - pass soup = _get_soup(series_url, session=session) base_series_url = _series_root_url(_extract_canonical_url(soup, series_url)) @@ -673,8 +670,10 @@ def scrape_series_detail( season_links = season_links[:max_seasons] seasons: List[SeasonInfo] = [] for number, url in season_links: - season_soup = _get_soup(url, session=session) - episodes = _extract_episodes(season_soup) + episodes: List[EpisodeInfo] = [] + if load_episodes: + season_soup = _get_soup(url, session=session) + episodes = _extract_episodes(season_soup) seasons.append(SeasonInfo(number=number, url=url, episodes=episodes)) seasons.sort(key=lambda s: s.number) return seasons @@ -689,6 +688,7 @@ class SerienstreamPlugin(BasisPlugin): def __init__(self) -> None: self._series_results: Dict[str, SeriesResult] = {} self._season_cache: Dict[str, List[SeasonInfo]] = {} + self._season_links_cache: Dict[str, List[SeasonInfo]] = {} self._episode_label_cache: Dict[Tuple[str, str], Dict[str, EpisodeInfo]] = {} self._catalog_cache: Optional[Dict[str, List[SeriesResult]]] = None self._popular_cache: Optional[List[SeriesResult]] = None @@ -830,21 +830,53 @@ class SerienstreamPlugin(BasisPlugin): self._episode_label(info): info for info in season_info.episodes } + def _ensure_season_links(self, title: str) -> List[SeasonInfo]: + cached = self._season_links_cache.get(title) + if cached is not None: + return list(cached) + series = self._series_results.get(title) + if not series: + return [] + try: + seasons = scrape_series_detail(series.url, load_episodes=False) + 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) + return list(seasons) + + def _ensure_season_episodes(self, title: str, season_number: int) -> Optional[SeasonInfo]: + seasons = self._season_cache.get(title) or [] + for season in seasons: + if season.number == season_number and season.episodes: + return season + links = self._ensure_season_links(title) + target = next((season for season in links if season.number == season_number), None) + if not target: + return None + try: + season_soup = _get_soup(target.url, session=get_requests_session("serienstream", headers=HEADERS)) + season_info = SeasonInfo(number=target.number, url=target.url, episodes=_extract_episodes(season_soup)) + except Exception as exc: # pragma: no cover - defensive logging + raise RuntimeError(f"Serienstream-Episoden konnten nicht geladen werden: {exc}") from exc + + updated = [season for season in seasons if season.number != season_number] + updated.append(season_info) + updated.sort(key=lambda item: item.number) + self._season_cache[title] = updated + return season_info + def _lookup_episode(self, title: str, season_label: str, episode_label: str) -> Optional[EpisodeInfo]: cache_key = (title, season_label) cached = self._episode_label_cache.get(cache_key) if cached: return cached.get(episode_label) - - seasons = self._ensure_seasons(title) number = self._parse_season_number(season_label) if number is None: return None - - for season_info in seasons: - if season_info.number == number: - self._cache_episode_labels(title, season_label, season_info) - return self._episode_label_cache.get(cache_key, {}).get(episode_label) + season_info = self._ensure_season_episodes(title, number) + if season_info: + self._cache_episode_labels(title, season_label, season_info) + return self._episode_label_cache.get(cache_key, {}).get(episode_label) return None async def search_titles(self, query: str) -> List[str]: @@ -852,6 +884,7 @@ class SerienstreamPlugin(BasisPlugin): if not query: self._series_results.clear() self._season_cache.clear() + self._season_links_cache.clear() self._episode_label_cache.clear() self._catalog_cache = None return [] @@ -869,6 +902,7 @@ class SerienstreamPlugin(BasisPlugin): raise RuntimeError(f"Serienstream-Suche fehlgeschlagen: {exc}") from exc self._series_results = {result.title: result for result in results} self._season_cache.clear() + self._season_links_cache.clear() self._episode_label_cache.clear() return [result.title for result in results] @@ -901,30 +935,24 @@ class SerienstreamPlugin(BasisPlugin): break if not series: return [] - try: - seasons = scrape_series_detail(series.url) - except Exception as exc: # pragma: no cover - defensive logging - raise RuntimeError(f"Serienstream-Staffeln konnten nicht geladen werden: {exc}") from exc + seasons = self._ensure_season_links(title) self._clear_episode_cache_for_title(title) - self._season_cache[title] = seasons - return seasons + self._season_cache[title] = list(seasons) + return list(seasons) def seasons_for(self, title: str) -> List[str]: seasons = self._ensure_seasons(title) - # Serienstream liefert gelegentlich Staffeln ohne Episoden (z.B. Parsing-/Layoutwechsel). - # Diese sollen im UI nicht als auswählbarer Menüpunkt erscheinen. - return [self._season_label(season.number) for season in seasons if season.episodes] + return [self._season_label(season.number) for season in seasons] def episodes_for(self, title: str, season: str) -> List[str]: - seasons = self._ensure_seasons(title) number = self._parse_season_number(season) if number is None: return [] - for season_info in seasons: - if season_info.number == number: - labels = [self._episode_label(info) for info in season_info.episodes] - self._cache_episode_labels(title, season, season_info) - return labels + season_info = self._ensure_season_episodes(title, number) + if season_info: + labels = [self._episode_label(info) for info in season_info.episodes] + self._cache_episode_labels(title, season, season_info) + return labels return [] def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]: