Speed up serienstream season and episode loading
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -6,3 +6,8 @@
|
|||||||
|
|
||||||
# Build outputs
|
# Build outputs
|
||||||
/dist/
|
/dist/
|
||||||
|
|
||||||
|
# Local tests (not committed)
|
||||||
|
/tests/
|
||||||
|
/.pytest_cache/
|
||||||
|
/pytest.ini
|
||||||
|
|||||||
@@ -644,17 +644,14 @@ def resolve_redirect(target_url: str) -> Optional[str]:
|
|||||||
def scrape_series_detail(
|
def scrape_series_detail(
|
||||||
series_identifier: str,
|
series_identifier: str,
|
||||||
max_seasons: Optional[int] = None,
|
max_seasons: Optional[int] = None,
|
||||||
|
*,
|
||||||
|
load_episodes: bool = True,
|
||||||
) -> List[SeasonInfo]:
|
) -> List[SeasonInfo]:
|
||||||
_ensure_requests()
|
_ensure_requests()
|
||||||
series_url = _series_root_url(_normalize_series_url(series_identifier))
|
series_url = _series_root_url(_normalize_series_url(series_identifier))
|
||||||
_log_url(series_url, kind="SERIES")
|
_log_url(series_url, kind="SERIES")
|
||||||
_notify_url(series_url)
|
_notify_url(series_url)
|
||||||
session = get_requests_session("serienstream", headers=HEADERS)
|
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)
|
soup = _get_soup(series_url, session=session)
|
||||||
|
|
||||||
base_series_url = _series_root_url(_extract_canonical_url(soup, series_url))
|
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]
|
season_links = season_links[:max_seasons]
|
||||||
seasons: List[SeasonInfo] = []
|
seasons: List[SeasonInfo] = []
|
||||||
for number, url in season_links:
|
for number, url in season_links:
|
||||||
season_soup = _get_soup(url, session=session)
|
episodes: List[EpisodeInfo] = []
|
||||||
episodes = _extract_episodes(season_soup)
|
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.append(SeasonInfo(number=number, url=url, episodes=episodes))
|
||||||
seasons.sort(key=lambda s: s.number)
|
seasons.sort(key=lambda s: s.number)
|
||||||
return seasons
|
return seasons
|
||||||
@@ -689,6 +688,7 @@ class SerienstreamPlugin(BasisPlugin):
|
|||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._series_results: Dict[str, SeriesResult] = {}
|
self._series_results: Dict[str, SeriesResult] = {}
|
||||||
self._season_cache: Dict[str, List[SeasonInfo]] = {}
|
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._episode_label_cache: Dict[Tuple[str, str], Dict[str, EpisodeInfo]] = {}
|
||||||
self._catalog_cache: Optional[Dict[str, List[SeriesResult]]] = None
|
self._catalog_cache: Optional[Dict[str, List[SeriesResult]]] = None
|
||||||
self._popular_cache: Optional[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
|
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]:
|
def _lookup_episode(self, title: str, season_label: str, episode_label: str) -> Optional[EpisodeInfo]:
|
||||||
cache_key = (title, season_label)
|
cache_key = (title, season_label)
|
||||||
cached = self._episode_label_cache.get(cache_key)
|
cached = self._episode_label_cache.get(cache_key)
|
||||||
if cached:
|
if cached:
|
||||||
return cached.get(episode_label)
|
return cached.get(episode_label)
|
||||||
|
|
||||||
seasons = self._ensure_seasons(title)
|
|
||||||
number = self._parse_season_number(season_label)
|
number = self._parse_season_number(season_label)
|
||||||
if number is None:
|
if number is None:
|
||||||
return None
|
return None
|
||||||
|
season_info = self._ensure_season_episodes(title, number)
|
||||||
for season_info in seasons:
|
if season_info:
|
||||||
if season_info.number == number:
|
self._cache_episode_labels(title, season_label, season_info)
|
||||||
self._cache_episode_labels(title, season_label, season_info)
|
return self._episode_label_cache.get(cache_key, {}).get(episode_label)
|
||||||
return self._episode_label_cache.get(cache_key, {}).get(episode_label)
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def search_titles(self, query: str) -> List[str]:
|
async def search_titles(self, query: str) -> List[str]:
|
||||||
@@ -852,6 +884,7 @@ class SerienstreamPlugin(BasisPlugin):
|
|||||||
if not query:
|
if not query:
|
||||||
self._series_results.clear()
|
self._series_results.clear()
|
||||||
self._season_cache.clear()
|
self._season_cache.clear()
|
||||||
|
self._season_links_cache.clear()
|
||||||
self._episode_label_cache.clear()
|
self._episode_label_cache.clear()
|
||||||
self._catalog_cache = None
|
self._catalog_cache = None
|
||||||
return []
|
return []
|
||||||
@@ -869,6 +902,7 @@ class SerienstreamPlugin(BasisPlugin):
|
|||||||
raise RuntimeError(f"Serienstream-Suche fehlgeschlagen: {exc}") from exc
|
raise RuntimeError(f"Serienstream-Suche fehlgeschlagen: {exc}") from exc
|
||||||
self._series_results = {result.title: result for result in results}
|
self._series_results = {result.title: result for result in results}
|
||||||
self._season_cache.clear()
|
self._season_cache.clear()
|
||||||
|
self._season_links_cache.clear()
|
||||||
self._episode_label_cache.clear()
|
self._episode_label_cache.clear()
|
||||||
return [result.title for result in results]
|
return [result.title for result in results]
|
||||||
|
|
||||||
@@ -901,30 +935,24 @@ class SerienstreamPlugin(BasisPlugin):
|
|||||||
break
|
break
|
||||||
if not series:
|
if not series:
|
||||||
return []
|
return []
|
||||||
try:
|
seasons = self._ensure_season_links(title)
|
||||||
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
|
|
||||||
self._clear_episode_cache_for_title(title)
|
self._clear_episode_cache_for_title(title)
|
||||||
self._season_cache[title] = seasons
|
self._season_cache[title] = list(seasons)
|
||||||
return seasons
|
return list(seasons)
|
||||||
|
|
||||||
def seasons_for(self, title: str) -> List[str]:
|
def seasons_for(self, title: str) -> List[str]:
|
||||||
seasons = self._ensure_seasons(title)
|
seasons = self._ensure_seasons(title)
|
||||||
# Serienstream liefert gelegentlich Staffeln ohne Episoden (z.B. Parsing-/Layoutwechsel).
|
return [self._season_label(season.number) for season in seasons]
|
||||||
# Diese sollen im UI nicht als auswählbarer Menüpunkt erscheinen.
|
|
||||||
return [self._season_label(season.number) for season in seasons if season.episodes]
|
|
||||||
|
|
||||||
def episodes_for(self, title: str, season: str) -> List[str]:
|
def episodes_for(self, title: str, season: str) -> List[str]:
|
||||||
seasons = self._ensure_seasons(title)
|
|
||||||
number = self._parse_season_number(season)
|
number = self._parse_season_number(season)
|
||||||
if number is None:
|
if number is None:
|
||||||
return []
|
return []
|
||||||
for season_info in seasons:
|
season_info = self._ensure_season_episodes(title, number)
|
||||||
if season_info.number == number:
|
if season_info:
|
||||||
labels = [self._episode_label(info) for info in season_info.episodes]
|
labels = [self._episode_label(info) for info in season_info.episodes]
|
||||||
self._cache_episode_labels(title, season, season_info)
|
self._cache_episode_labels(title, season, season_info)
|
||||||
return labels
|
return labels
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
|
def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]:
|
||||||
|
|||||||
Reference in New Issue
Block a user