serienstream: source metadata for seasons/episodes
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.53" provider-name="ViewIt">
|
||||
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.54" provider-name="ViewIt">
|
||||
<requires>
|
||||
<import addon="xbmc.python" version="3.0.0" />
|
||||
<import addon="script.module.requests" />
|
||||
|
||||
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user