From 4e9ae348b9221b1fc959556b2bdf07b275a92a62 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Sun, 15 Mar 2026 15:56:53 +0100 Subject: [PATCH] dev: SerienStream Suche via /suche?term=, Staffel 0 als Filme, Katalog-Suche entfernt --- CHANGELOG-DEV.md | 4 + addon/addon.xml | 2 +- addon/plugins/serienstream_plugin.py | 164 +++------------------------ addon/resources/settings.xml | 3 +- 4 files changed, 24 insertions(+), 149 deletions(-) diff --git a/CHANGELOG-DEV.md b/CHANGELOG-DEV.md index 743fc6b..dd0bf6f 100644 --- a/CHANGELOG-DEV.md +++ b/CHANGELOG-DEV.md @@ -1,3 +1,7 @@ +## 0.1.83.0-dev - 2026-03-15 + +- dev: Trakt Performance, Suchfilter Phrase-Match, Debug-Settings Expert-Level + ## 0.1.82.5-dev - 2026-03-15 - dev: Update-Versionsvergleich numerisch korrigiert diff --git a/addon/addon.xml b/addon/addon.xml index f10c8e8..f44627f 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 35293a6..a8c0da7 100644 --- a/addon/plugins/serienstream_plugin.py +++ b/addon/plugins/serienstream_plugin.py @@ -57,7 +57,6 @@ else: # pragma: no cover SETTING_BASE_URL = "serienstream_base_url" -SETTING_CATALOG_SEARCH = "serienstream_catalog_search" DEFAULT_BASE_URL = "https://s.to" DEFAULT_PREFERRED_HOSTERS = ["voe"] DEFAULT_TIMEOUT = 20 @@ -80,10 +79,7 @@ HEADERS = { SESSION_CACHE_TTL_SECONDS = 300 SESSION_CACHE_PREFIX = "viewit.serienstream" SESSION_CACHE_MAX_TITLE_URLS = 800 -CATALOG_SEARCH_TTL_SECONDS = 600 -CATALOG_SEARCH_CACHE_KEY = "catalog_index" GENRE_LIST_PAGE_SIZE = 20 -_CATALOG_INDEX_MEMORY: tuple[float, list["SeriesResult"]] = (0.0, []) ProgressCallback = Optional[Callable[[str, int | None], Any]] @@ -575,8 +571,8 @@ def _search_series_server(query: str) -> list[SeriesResult]: if not query: return [] base = _get_base_url() - search_url = f"{base}/search?q={quote(query)}" - alt_url = f"{base}/suche?q={quote(query)}" + search_url = f"{base}/suche?term={quote(query)}" + alt_url = f"{base}/search?term={quote(query)}" for url in (search_url, alt_url): try: body = _get_html_simple(url) @@ -606,158 +602,30 @@ def _search_series_server(query: str) -> list[SeriesResult]: continue seen_urls.add(url_abs) results.append(SeriesResult(title=title, description="", url=url_abs)) + filtered = [r for r in results if _matches_query(query, title=r.title)] + if filtered: + return filtered if results: return results - api_results = _search_series_api(query) - if api_results: - return api_results return [] -def _extract_catalog_index_from_html(body: str, *, progress_callback: ProgressCallback = None) -> list[SeriesResult]: - items: list[SeriesResult] = [] - if not body: - return items - seen_urls: set[str] = set() - item_re = re.compile( - r"]*class=[\"'][^\"']*series-item[^\"']*[\"'][^>]*>(.*?)", - re.IGNORECASE | re.DOTALL, - ) - anchor_re = re.compile(r"]+href=[\"']([^\"']+)[\"'][^>]*>(.*?)", re.IGNORECASE | re.DOTALL) - data_search_re = re.compile(r"data-search=[\"']([^\"']*)[\"']", re.IGNORECASE) - for idx, match in enumerate(item_re.finditer(body), start=1): - if idx == 1 or idx % 200 == 0: - _emit_progress(progress_callback, f"Katalog parsen {idx}", 62) - block = match.group(0) - inner = match.group(1) or "" - anchor_match = anchor_re.search(inner) - if not anchor_match: - continue - href = (anchor_match.group(1) or "").strip() - url = _absolute_url(href) - if not url or "/serie/" not in url or "/staffel-" in url or "/episode-" in url: - continue - if url in seen_urls: - continue - seen_urls.add(url) - title_raw = anchor_match.group(2) or "" - title = unescape(re.sub(r"\s+", " ", _strip_tags(title_raw))).strip() - if not title: - continue - search_match = data_search_re.search(block) - description = (search_match.group(1) or "").strip() if search_match else "" - items.append(SeriesResult(title=title, description=description, url=url)) - return items - - -def _catalog_index_from_soup(soup: BeautifulSoupT) -> list[SeriesResult]: - items: list[SeriesResult] = [] - if not soup: - return items - seen_urls: set[str] = set() - for item in soup.select("li.series-item"): - anchor = item.find("a", href=True) - if not anchor: - continue - href = (anchor.get("href") or "").strip() - url = _absolute_url(href) - if not url or "/serie/" not in url or "/staffel-" in url or "/episode-" in url: - continue - if url in seen_urls: - continue - seen_urls.add(url) - title = (anchor.get_text(" ", strip=True) or "").strip() - if not title: - continue - description = (item.get("data-search") or "").strip() - items.append(SeriesResult(title=title, description=description, url=url)) - return items - - -def _load_catalog_index_from_cache() -> Optional[list[SeriesResult]]: - global _CATALOG_INDEX_MEMORY - expires_at, cached = _CATALOG_INDEX_MEMORY - if cached and expires_at > time.time(): - return list(cached) - raw = _session_cache_get(CATALOG_SEARCH_CACHE_KEY) - if not isinstance(raw, list): - return None - items: list[SeriesResult] = [] - for entry in raw: - if not isinstance(entry, list) or len(entry) < 2: - continue - title = str(entry[0] or "").strip() - url = str(entry[1] or "").strip() - description = str(entry[2] or "") if len(entry) > 2 else "" - cover = str(entry[3] or "").strip() if len(entry) > 3 else "" - if title and url: - items.append(SeriesResult(title=title, description=description, url=url, cover=cover)) - if items: - _CATALOG_INDEX_MEMORY = (time.time() + CATALOG_SEARCH_TTL_SECONDS, list(items)) - return items or None - - -def _store_catalog_index_in_cache(items: list[SeriesResult]) -> None: - global _CATALOG_INDEX_MEMORY - if not items: - return - _CATALOG_INDEX_MEMORY = (time.time() + CATALOG_SEARCH_TTL_SECONDS, list(items)) - payload: list[list[str]] = [] - for entry in items: - if not entry.title or not entry.url: - continue - payload.append([entry.title, entry.url, entry.description, entry.cover]) - _session_cache_set(CATALOG_SEARCH_CACHE_KEY, payload, ttl_seconds=CATALOG_SEARCH_TTL_SECONDS) - def search_series(query: str, *, progress_callback: ProgressCallback = None) -> list[SeriesResult]: - """Sucht Serien. Katalog-Suche (vollstaendig) oder API-Suche (max 10) je nach Setting.""" + """Sucht Serien. Server-Suche (/suche?term=) zuerst, API als Fallback.""" _ensure_requests() if not _normalize_search_text(query): return [] - use_catalog = _get_setting_bool(SETTING_CATALOG_SEARCH, default=True) - - if use_catalog: - _emit_progress(progress_callback, "Pruefe Such-Cache", 15) - cached = _load_catalog_index_from_cache() - if cached is not None: - matched_from_cache = [entry for entry in cached if entry.title and _matches_query(query, title=entry.title)] - _emit_progress(progress_callback, f"Cache-Treffer: {len(cached)}", 35) - if matched_from_cache: - return matched_from_cache - - _emit_progress(progress_callback, "Lade Katalogseite", 42) - catalog_url = f"{_get_base_url()}/serien?by=genre" - items: list[SeriesResult] = [] - try: - soup = _get_soup_simple(catalog_url) - items = _catalog_index_from_soup(soup) - except Exception: - body = _get_html_simple(catalog_url) - items = _extract_catalog_index_from_html(body, progress_callback=progress_callback) - if not items: - _emit_progress(progress_callback, "Fallback-Parser", 58) - soup = BeautifulSoup(body, "html.parser") - items = _catalog_index_from_soup(soup) - if items: - _store_catalog_index_in_cache(items) - _emit_progress(progress_callback, f"Filtere Treffer ({len(items)})", 70) - return [entry for entry in items if entry.title and _matches_query(query, title=entry.title)] - - # API-Suche (primaer wenn Katalog deaktiviert, Fallback wenn Katalog leer) - _emit_progress(progress_callback, "API-Suche", 60) - api_results = _search_series_api(query) - if api_results: - _emit_progress(progress_callback, f"API-Treffer: {len(api_results)}", 80) - return api_results - - _emit_progress(progress_callback, "Server-Suche", 85) + # 1. Server-Suche (schnell, vollstaendig, direkte HTML-Suche) + _emit_progress(progress_callback, "Suche", 20) server_results = _search_series_server(query) if server_results: - _emit_progress(progress_callback, f"Server-Treffer: {len(server_results)}", 95) - return [entry for entry in server_results if entry.title and _matches_query(query, title=entry.title)] - return [] + return server_results + + # 2. API-Suche (Fallback, max 10 Ergebnisse) + _emit_progress(progress_callback, "API-Suche", 60) + return _search_series_api(query) def parse_series_catalog(soup: BeautifulSoupT) -> dict[str, list[SeriesResult]]: @@ -1252,7 +1120,7 @@ class SerienstreamPlugin(BasisPlugin): except Exception: continue url = str(item.get("url") or "").strip() - if number <= 0 or not url: + if number < 0 or not url: continue seasons.append(SeasonInfo(number=number, url=url, episodes=[])) if not seasons: @@ -1794,6 +1662,8 @@ class SerienstreamPlugin(BasisPlugin): @staticmethod def _season_label(number: int) -> str: + if number == 0: + return "Filme" return f"Staffel {number}" @staticmethod @@ -1808,6 +1678,8 @@ class SerienstreamPlugin(BasisPlugin): @staticmethod def _parse_season_number(label: str) -> int | None: + if (label or "").strip().casefold() == "filme": + return 0 digits = "".join(ch for ch in label if ch.isdigit()) if not digits: return None diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index b7ea060..29a9a7f 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -2,8 +2,7 @@ - - +