From cd2e8e2b15624295e4db0de961703218f9e48e90 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Sun, 1 Feb 2026 18:25:22 +0100 Subject: [PATCH] Standardize plugin base URLs --- addon/default.py | 1 + addon/plugins/aniworld_plugin.py | 64 +++++++++++++++++++--------- addon/plugins/serienstream_plugin.py | 47 +++++++++++++------- addon/resources/settings.xml | 10 ++++- 4 files changed, 86 insertions(+), 36 deletions(-) diff --git a/addon/default.py b/addon/default.py index 191f345..4159178 100644 --- a/addon/default.py +++ b/addon/default.py @@ -12,6 +12,7 @@ from contextlib import contextmanager from datetime import datetime import importlib.util import inspect +import json import os import re import sys diff --git a/addon/plugins/aniworld_plugin.py b/addon/plugins/aniworld_plugin.py index 99d7a65..b3f42c2 100644 --- a/addon/plugins/aniworld_plugin.py +++ b/addon/plugins/aniworld_plugin.py @@ -29,7 +29,7 @@ except ImportError: # pragma: no cover - allow running outside Kodi xbmcaddon = None from plugin_interface import BasisPlugin -from plugin_helpers import dump_response_html, get_setting_bool, log_url, notify_url +from plugin_helpers import dump_response_html, get_setting_bool, get_setting_string, log_url, notify_url from http_session_pool import get_requests_session from regex_patterns import DIGITS, SEASON_EPISODE_TAG, SEASON_EPISODE_URL, STAFFEL_NUM_IN_URL @@ -41,13 +41,8 @@ else: # pragma: no cover BeautifulSoupT: TypeAlias = Any -BASE_URL = "https://aniworld.to" -ANIME_BASE_URL = f"{BASE_URL}/anime/stream" -POPULAR_ANIMES_URL = f"{BASE_URL}/beliebte-animes" -GENRES_URL = f"{BASE_URL}/animes" -LATEST_EPISODES_URL = f"{BASE_URL}/neue-episoden" -SEARCH_URL = f"{BASE_URL}/search?q={{query}}" -SEARCH_API_URL = f"{BASE_URL}/ajax/search" +SETTING_BASE_URL = "aniworld_base_url" +DEFAULT_BASE_URL = "https://aniworld.to" DEFAULT_PREFERRED_HOSTERS = ["voe"] DEFAULT_TIMEOUT = 20 ADDON_ID = "plugin.video.viewit" @@ -93,8 +88,39 @@ class SeasonInfo: episodes: List[EpisodeInfo] +def _get_base_url() -> str: + base = get_setting_string(ADDON_ID, SETTING_BASE_URL, default=DEFAULT_BASE_URL).strip() + if not base: + base = DEFAULT_BASE_URL + return base.rstrip("/") + + +def _anime_base_url() -> str: + return f"{_get_base_url()}/anime/stream" + + +def _popular_animes_url() -> str: + return f"{_get_base_url()}/beliebte-animes" + + +def _genres_url() -> str: + return f"{_get_base_url()}/animes" + + +def _latest_episodes_url() -> str: + return f"{_get_base_url()}/neue-episoden" + + +def _search_url(query: str) -> str: + return f"{_get_base_url()}/search?q={query}" + + +def _search_api_url() -> str: + return f"{_get_base_url()}/ajax/search" + + def _absolute_url(href: str) -> str: - return f"{BASE_URL}{href}" if href.startswith("/") else href + return f"{_get_base_url()}{href}" if href.startswith("/") else href def _log_url(url: str, *, kind: str = "VISIT") -> None: @@ -360,7 +386,7 @@ def scrape_anime_detail(anime_identifier: str, max_seasons: Optional[int] = None _log_url(anime_url, kind="ANIME") session = get_requests_session("aniworld", headers=HEADERS) try: - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) except Exception: pass soup = _get_soup(anime_url, session=session) @@ -394,7 +420,7 @@ def resolve_redirect(target_url: str) -> Optional[str]: normalized_url = _absolute_url(target_url) _log_visit(normalized_url) session = get_requests_session("aniworld", headers=HEADERS) - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) response = session.get(normalized_url, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True) if response.url: _log_url(response.url, kind="RESOLVED") @@ -405,7 +431,7 @@ def fetch_episode_hoster_names(episode_url: str) -> List[str]: _ensure_requests() normalized_url = _absolute_url(episode_url) session = get_requests_session("aniworld", headers=HEADERS) - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) soup = _get_soup(normalized_url, session=session) names: List[str] = [] seen: set[str] = set() @@ -440,7 +466,7 @@ def fetch_episode_stream_link( normalized_url = _absolute_url(episode_url) preferred = [hoster.lower() for hoster in (preferred_hosters or DEFAULT_PREFERRED_HOSTERS)] session = get_requests_session("aniworld", headers=HEADERS) - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) soup = _get_soup(normalized_url, session=session) candidates: List[Tuple[str, str]] = [] for anchor in soup.select(".hosterSiteVideo a.watchEpisode"): @@ -476,10 +502,10 @@ def search_animes(query: str) -> List[SeriesResult]: return [] session = get_requests_session("aniworld", headers=HEADERS) try: - session.get(BASE_URL, headers=HEADERS, timeout=DEFAULT_TIMEOUT) + session.get(_get_base_url(), headers=HEADERS, timeout=DEFAULT_TIMEOUT) except Exception: pass - data = _post_json(SEARCH_API_URL, payload={"keyword": query}, session=session) + data = _post_json(_search_api_url(), payload={"keyword": query}, session=session) results: List[SeriesResult] = [] seen: set[str] = set() if isinstance(data, list): @@ -507,7 +533,7 @@ def search_animes(query: str) -> List[SeriesResult]: results.append(SeriesResult(title=title, description=description, url=url)) return results - soup = _get_soup_simple(SEARCH_URL.format(query=requests.utils.quote(query))) + soup = _get_soup_simple(_search_url(requests.utils.quote(query))) for anchor in soup.select("a[href^='/anime/stream/'][href]"): href = (anchor.get("href") or "").strip() if not href or "/staffel-" in href or "/episode-" in href: @@ -600,7 +626,7 @@ class AniworldPlugin(BasisPlugin): def _ensure_popular(self) -> List[SeriesResult]: if self._popular_cache is not None: return list(self._popular_cache) - soup = _get_soup_simple(POPULAR_ANIMES_URL) + soup = _get_soup_simple(_popular_animes_url()) results: List[SeriesResult] = [] seen: set[str] = set() for anchor in soup.select("div.seriesListContainer a[href^='/anime/stream/']"): @@ -646,7 +672,7 @@ class AniworldPlugin(BasisPlugin): if cached is not None: return list(cached) - url = LATEST_EPISODES_URL + url = _latest_episodes_url() if page > 1: url = f"{url}?page={page}" @@ -658,7 +684,7 @@ class AniworldPlugin(BasisPlugin): def _ensure_genres(self) -> Dict[str, List[SeriesResult]]: if self._genre_cache is not None: return {key: list(value) for key, value in self._genre_cache.items()} - soup = _get_soup_simple(GENRES_URL) + soup = _get_soup_simple(_genres_url()) results: Dict[str, List[SeriesResult]] = {} genre_blocks = soup.select("#seriesContainer div.genre") if not genre_blocks: diff --git a/addon/plugins/serienstream_plugin.py b/addon/plugins/serienstream_plugin.py index 8f139dc..be4a194 100644 --- a/addon/plugins/serienstream_plugin.py +++ b/addon/plugins/serienstream_plugin.py @@ -37,7 +37,7 @@ except ImportError: # pragma: no cover - allow running outside Kodi xbmcgui = None from plugin_interface import BasisPlugin -from plugin_helpers import dump_response_html, get_setting_bool, log_url, notify_url +from plugin_helpers import dump_response_html, get_setting_bool, get_setting_string, log_url, notify_url from http_session_pool import get_requests_session from regex_patterns import SEASON_EPISODE_TAG, SEASON_EPISODE_URL @@ -49,10 +49,8 @@ else: # pragma: no cover BeautifulSoupT: TypeAlias = Any -BASE_URL = "https://s.to" -SERIES_BASE_URL = f"{BASE_URL}/serie/stream" -POPULAR_SERIES_URL = f"{BASE_URL}/beliebte-serien" -LATEST_EPISODES_URL = f"{BASE_URL}" +SETTING_BASE_URL = "serienstream_base_url" +DEFAULT_BASE_URL = "https://s.to" DEFAULT_PREFERRED_HOSTERS = ["voe"] DEFAULT_TIMEOUT = 20 ADDON_ID = "plugin.video.viewit" @@ -101,15 +99,34 @@ class SeasonInfo: episodes: List[EpisodeInfo] +def _get_base_url() -> str: + base = get_setting_string(ADDON_ID, SETTING_BASE_URL, default=DEFAULT_BASE_URL).strip() + if not base: + base = DEFAULT_BASE_URL + return base.rstrip("/") + + +def _series_base_url() -> str: + return f"{_get_base_url()}/serie/stream" + + +def _popular_series_url() -> str: + return f"{_get_base_url()}/beliebte-serien" + + +def _latest_episodes_url() -> str: + return f"{_get_base_url()}" + + def _absolute_url(href: str) -> str: - return f"{BASE_URL}{href}" if href.startswith("/") else href + return f"{_get_base_url()}{href}" if href.startswith("/") else href def _normalize_series_url(identifier: str) -> str: if identifier.startswith("http://") or identifier.startswith("https://"): return identifier.rstrip("/") slug = identifier.strip("/") - return f"{SERIES_BASE_URL}/{slug}" + return f"{_series_base_url()}/{slug}" def _series_root_url(url: str) -> str: @@ -227,7 +244,7 @@ def search_series(query: str) -> List[SeriesResult]: if not normalized_query: return [] # Direkter Abruf wie in fetch_serien.py. - catalog_url = f"{BASE_URL}/serien?by=genre" + catalog_url = f"{_get_base_url()}/serien?by=genre" soup = _get_soup_simple(catalog_url) results: List[SeriesResult] = [] for series in parse_series_catalog(soup).values(): @@ -424,7 +441,7 @@ def fetch_episode_stream_link( session = get_requests_session("serienstream", headers=HEADERS) # Preflight optional: Startseite kann 5xx liefern, Zielseite aber funktionieren. try: - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) except Exception: pass soup = _get_soup(normalized_url, session=session) @@ -453,7 +470,7 @@ def fetch_episode_hoster_names(episode_url: str) -> List[str]: session = get_requests_session("serienstream", headers=HEADERS) # Preflight optional: Startseite kann 5xx liefern, Zielseite aber funktionieren. try: - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) except Exception: pass soup = _get_soup(normalized_url, session=session) @@ -546,7 +563,7 @@ def resolve_redirect(target_url: str) -> Optional[str]: session = get_requests_session("serienstream", headers=HEADERS) # Preflight optional: Startseite kann 5xx liefern, Zielseite aber funktionieren. try: - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) except Exception: pass response = session.get( @@ -571,7 +588,7 @@ def scrape_series_detail( session = get_requests_session("serienstream", headers=HEADERS) # Preflight ist optional; manche Umgebungen/Provider leiten die Startseite um. try: - _get_soup(BASE_URL, session=session) + _get_soup(_get_base_url(), session=session) except Exception: pass soup = _get_soup(series_url, session=session) @@ -636,7 +653,7 @@ class SerienstreamPlugin(BasisPlugin): if self._catalog_cache is not None: return self._catalog_cache # Stand: 2026-01 liefert `?by=genre` konsistente Gruppen für `genres()`. - catalog_url = f"{BASE_URL}/serien?by=genre" + catalog_url = f"{_get_base_url()}/serien?by=genre" soup = _get_soup_simple(catalog_url) self._catalog_cache = parse_series_catalog(soup) return self._catalog_cache @@ -678,7 +695,7 @@ class SerienstreamPlugin(BasisPlugin): """Laedt und cached die Liste der beliebten Serien aus `/beliebte-serien`.""" if self._popular_cache is not None: return list(self._popular_cache) - soup = _get_soup_simple(POPULAR_SERIES_URL) + soup = _get_soup_simple(_popular_series_url()) results: List[SeriesResult] = [] seen: set[str] = set() @@ -894,7 +911,7 @@ class SerienstreamPlugin(BasisPlugin): if cached is not None: return list(cached) - url = LATEST_EPISODES_URL + url = _latest_episodes_url() if page > 1: url = f"{url}?page={page}" soup = _get_soup_simple(url) diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index efe74a3..7f2b1f2 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -6,11 +6,17 @@ - + + + + + + + - +