diff --git a/CHANGELOG-DEV.md b/CHANGELOG-DEV.md index 7295e48..cf603ca 100644 --- a/CHANGELOG-DEV.md +++ b/CHANGELOG-DEV.md @@ -1,3 +1,7 @@ +## 0.1.84.5-dev - 2026-03-31 + +- dev: bump to 0.1.84.0-dev SerienStream Sammlungen mit Poster/Plot, Session-Cache für Sammlungs-URLs + ## 0.1.84.0-dev - 2026-03-16 - dev: bump to 0.1.83.5-dev Trakt Weiterschauen via watched/shows, Specials überspringen diff --git a/addon/addon.xml b/addon/addon.xml index f31d34a..2794bd5 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/default.py b/addon/default.py index 4a80f8b..1a1fa5a 100644 --- a/addon/default.py +++ b/addon/default.py @@ -205,6 +205,19 @@ def _set_trakt_ids_property(title: str, tmdb_id: int, imdb_id: str = "") -> None # Trakt-Helfer # --------------------------------------------------------------------------- +_PREFERRED_HOSTERS_LIST = ["voe", "streamtape", "doodstream", "vidoza", "mixdrop", "supervideo", "dropload"] + + +def _get_preferred_hoster() -> str: + """Liest preferred_hoster (enum-Index) und gibt den Hosternamen zurück.""" + raw = _get_setting_string("preferred_hoster").strip() + try: + idx = int(raw) + return _PREFERRED_HOSTERS_LIST[idx] + except (ValueError, IndexError): + return raw # Fallback: alten Textwert direkt verwenden + + def _trakt_load_token(): """Laedt den gespeicherten Trakt-Token aus den Addon-Settings.""" access = _get_setting_string("trakt_access_token").strip() @@ -225,6 +238,7 @@ def _trakt_save_token(token) -> None: addon.setSetting("trakt_access_token", token.access_token) addon.setSetting("trakt_refresh_token", token.refresh_token) addon.setSetting("trakt_token_expires", str(token.expires_at)) + addon.setSetting("trakt_status", "Verbunden" if token.access_token else "Nicht verbunden") TRAKT_CLIENT_ID = "5f1a46be11faa2ef286d6a5d4fbdcdfe3b19c87d3799c11af8cf25dae5b802e9" @@ -1542,6 +1556,13 @@ def _sync_update_channel_status_settings() -> None: _set_setting_string("update_active_channel", _channel_label(channel)) _set_setting_string("update_active_repo_url", selected_info_url) _set_setting_string("update_available_selected", available_selected) + installed = _get_setting_string("update_installed_version").strip() + has_update = ( + bool(available_selected) and available_selected not in ("-", "", "0.0.0") + and bool(installed) and installed not in ("-", "", "0.0.0") + and _version_sort_key(available_selected) > _version_sort_key(installed) + ) + _get_addon().setSettingBool("update_available_flag", has_update) def _repo_addon_xml_path() -> str: @@ -4388,7 +4409,7 @@ def _play_episode( selected_hoster: str | None = None forced_hoster = (forced_hoster or "").strip() autoplay = _get_setting_bool("autoplay_enabled", default=False) - preferred = _get_setting_string("preferred_hoster").strip() + preferred = _get_preferred_hoster() if available_hosters: if forced_hoster: for hoster in available_hosters: @@ -4507,7 +4528,7 @@ def _play_episode_url( selected_hoster: str | None = None autoplay = _get_setting_bool("autoplay_enabled", default=False) - preferred = _get_setting_string("preferred_hoster").strip() + preferred = _get_preferred_hoster() if available_hosters: if autoplay and preferred: pref_lower = preferred.casefold() @@ -4716,28 +4737,34 @@ def _show_country_titles_page(plugin_name: str, country: str, page: int = 1) -> xbmcplugin.endOfDirectory(handle) -def _show_collections_menu(plugin_name: str) -> None: - """Zeigt Sammlungen/Filmreihen eines Plugins (Capability: collections).""" +def _show_collections_menu(plugin_name: str, page: int = 1) -> None: + """Zeigt Sammlungen/Filmreihen eines Plugins (Capability: collections) - paginiert.""" handle = _get_handle() plugin = _discover_plugins().get(plugin_name) if plugin is None: xbmcgui.Dialog().notification("Sammlungen", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcplugin.endOfDirectory(handle) return - getter = getattr(plugin, "collections", None) - if not callable(getter): + page_getter = getattr(plugin, "_collections_page", None) + if not callable(page_getter): xbmcplugin.endOfDirectory(handle) return - xbmcplugin.setPluginCategory(handle, f"{plugin_name}: Sammlungen") + xbmcplugin.setPluginCategory(handle, f"{plugin_name}: Sammlungen (Seite {page})") try: - cols = list(getter() or []) + cols = list(page_getter(page) or []) except Exception as exc: - _log(f"Sammlungen konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING) + _log(f"Sammlungen (Seite {page}) konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING) xbmcplugin.endOfDirectory(handle) return + if page > 1: + _add_directory_item(handle, "Vorherige Seite", "collections_menu", + {"plugin": plugin_name, "page": str(page - 1)}, is_folder=True) for col in cols: _add_directory_item(handle, str(col), "collection_titles_page", {"plugin": plugin_name, "collection": str(col), "page": "1"}, is_folder=True) + if cols: + _add_directory_item(handle, "Naechste Seite", "collections_menu", + {"plugin": plugin_name, "page": str(page + 1)}, is_folder=True) xbmcplugin.endOfDirectory(handle) @@ -4792,8 +4819,9 @@ def _show_collection_titles_page(plugin_name: str, collection: str, page: int = _add_directory_item(handle, title, "seasons", {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, is_folder=True, info_labels=info_labels, art=art, cast=cast) - _add_directory_item(handle, "Naechste Seite", "collection_titles_page", - {"plugin": plugin_name, "collection": collection, "page": str(page + 1)}, is_folder=True) + if getattr(plugin, "_collection_has_more", False): + _add_directory_item(handle, "Naechste Seite", "collection_titles_page", + {"plugin": plugin_name, "collection": collection, "page": str(page + 1)}, is_folder=True) xbmcplugin.endOfDirectory(handle) @@ -5438,7 +5466,10 @@ def _route_country_titles_page(params: dict[str, str]) -> None: @_router.route("collections_menu") def _route_collections_menu(params: dict[str, str]) -> None: - _show_collections_menu(params.get("plugin", "")) + _show_collections_menu( + params.get("plugin", ""), + _parse_positive_int(params.get("page", "1"), default=1), + ) @_router.route("collection_titles_page") @@ -5540,12 +5571,19 @@ def _route_fallback(params: dict[str, str]) -> None: _show_root_menu() +def _sync_trakt_status_setting() -> None: + """Setzt trakt_status anhand des gespeicherten Tokens.""" + connected = bool(_trakt_load_token()) + _get_addon().setSetting("trakt_status", "Verbunden" if connected else "Nicht verbunden") + + def run() -> None: params = _parse_params() action = params.get("action") _log(f"Action: {action}", xbmc.LOGDEBUG) _maybe_run_auto_update_check(action) _maybe_auto_install_resolveurl(action) + _sync_trakt_status_setting() _router.dispatch(action=action, params=params) diff --git a/addon/plugins/serienstream_plugin.py b/addon/plugins/serienstream_plugin.py index 0e21e91..377bb79 100644 --- a/addon/plugins/serienstream_plugin.py +++ b/addon/plugins/serienstream_plugin.py @@ -505,6 +505,14 @@ def _strip_tags(value: str) -> str: return re.sub(r"<[^>]+>", " ", value or "") +def _clean_collection_title(title: str) -> str: + cleaned = "".join( + ch for ch in title + if unicodedata.category(ch) not in ("So", "Sm", "Sk", "Sc", "Cs", "Co", "Cn") + ) + return re.sub(r"\s+", " ", cleaned).strip() + + def _search_series_api(query: str) -> list[SeriesResult]: query = (query or "").strip() if not query: @@ -1028,6 +1036,7 @@ class SerienstreamPlugin(BasisPlugin): self._series_metadata_cache: dict[str, tuple[dict[str, str], dict[str, str]]] = {} self._series_metadata_full: set[str] = set() self._collection_url_cache: dict[str, str] = {} + self._collection_has_more: bool = False self.is_available = True self.unavailable_reason: str | None = None if not self._requests_available: # pragma: no cover - optional dependency @@ -1255,34 +1264,35 @@ class SerienstreamPlugin(BasisPlugin): return {"popular_series", "genres", "latest_episodes", "alpha", "collections"} def collections(self) -> list[str]: - """Liefert alle Sammlungs-Namen von /sammlungen (alle Seiten).""" + """Liefert Sammlungs-Namen von /sammlungen (Seite 1, für Paginierung).""" + return self._collections_page(1) + + def _collections_page(self, page: int = 1) -> list[str]: + """Liefert eine Seite mit Sammlungs-Namen von /sammlungen (paginiert).""" if not self._requests_available: return [] base = _get_base_url() names: list[str] = [] url_map: dict[str, str] = {} - page = 1 - while True: - url = f"{base}/sammlungen" if page == 1 else f"{base}/sammlungen?page={page}" - soup = _get_soup_simple(url) - found = False - for a in soup.select('a[href*="/sammlung/"]'): - h2 = a.find("h2") - if not h2: - continue - title = h2.get_text(strip=True) - href = (a.get("href") or "").strip() - if title and href: - url_map[title] = _absolute_url(href) - names.append(title) - found = True - if not found: - break - if not soup.select(f'a[href*="/sammlungen?page={page + 1}"]'): - break - page += 1 + url = f"{base}/sammlungen" if page == 1 else f"{base}/sammlungen?page={page}" + soup = _get_soup_simple(url) + for a in soup.select('a[href*="/sammlung/"]'): + h2 = a.find("h2") + if not h2: + continue + title = _clean_collection_title(h2.get_text(strip=True)) + href = (a.get("href") or "").strip() + if title and href: + url_map[title] = _absolute_url(href) + names.append(title) if url_map: - _session_cache_set("collection_urls", url_map) + existing = _session_cache_get("collection_urls") + if isinstance(existing, dict): + existing.update(url_map) + _session_cache_set("collection_urls", existing) + else: + _session_cache_set("collection_urls", url_map) + names.sort(key=lambda t: t.casefold()) return names def titles_for_collection(self, collection: str, page: int = 1) -> list[str]: @@ -1297,6 +1307,7 @@ class SerienstreamPlugin(BasisPlugin): return [] if page > 1: url = f"{url}?page={page}" + base_url = self._collection_url_cache[collection] soup = _get_soup_simple(url) titles: list[str] = [] for a in soup.select('h6 a[href*="/serie/"]'): @@ -1305,6 +1316,7 @@ class SerienstreamPlugin(BasisPlugin): if title and href: self._remember_series_result(title, _absolute_url(href), "") titles.append(title) + self._collection_has_more = bool(soup.select(f'a[href*="?page={page + 1}"]')) return titles def popular_series(self) -> list[str]: diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index 29a9a7f..c393f45 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -1,130 +1,861 @@ - - - - - - - - - + +
- - - - - - - - - - - - - - - - - + + + + + 0 + false + + + + 0 + 0 + + + + + + + + + + + + + + true + + + + + + - - - - - - - - - - + + + + + 0 + false + + + + 0 + Nicht verbunden + + + false + + + + Status + + + + 0 + RunPlugin(plugin://plugin.video.viewit/?action=trakt_auth) + + + + 0 + true + + + true + + + + + + 0 + false + + + true + + + + + + + + 0 + + + + false + + + + + + 0 + + + + false + + + + + + 0 + 0 + + + false + + + + + + - - - - - - - - - - + + + + + 0 + true + + + + 0 + de-DE + + + true + + + + TMDB Sprache + + + + 0 + true + + + true + + + + + + 0 + true + + + true + + + + + + 0 + true + + + true + + + + + + 0 + true + + + true + + + + + + 0 + false + + + true + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + 2 + 0 + + + + + + + + + + + + - - - - + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + 2 + 15 + + 5 + 5 + 100 + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + 0 + false + + + false + + + + + + 0 + - + + + false + + + + + + 0 + - + + + false + + + true + + + + + + 0 + 1 + + + + + + + + + + + + 0 + RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel) + + + + 0 + false + + + + 0 + 1 + + + + + + + + + + true + + + + + + 0 + RunPlugin(plugin://plugin.video.viewit/?action=select_update_version) + + + + 0 + RunPlugin(plugin://plugin.video.viewit/?action=install_resolveurl) + + + + 0 + true + + + + 2 + - + + + false + + + + + + + + 3 + - + + + false + + + + + + 3 + - + + + false + + + + + + 3 + https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml + + Main URL + + + + 3 + https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml + + Nightly URL + + + + 3 + https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/dev/addons.xml + + Dev URL + + + + 3 + https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml + + Custom URL + + + + 0 + 0 + + + false + + + + + + 0 + 0 + + + false + + + + + + - - - - - - - - - + + + + + 0 + 0 + + + + + + + + + + + + + 0 + RunPlugin(plugin://plugin.video.viewit/?action=install_ytdlp) + + + + 2 + - + + + false + + + + + + - - - - - - - - - + + + + + 3 + https://s.to + + SerienStream URL + + + + 3 + https://aniworld.to + + AniWorld URL + + + + 3 + https://topstreamfilm.live + + TopStream URL + + + + 3 + https://einschalten.in + + Einschalten URL + + + + 3 + https://filmpalast.to + + Filmpalast URL + + + + 3 + https://doku-streams.com + + Doku-Streams URL + + + + - - - - - + + + + + 3 + + + true + + + TMDB API Key + + + + 3 + + + + false + + + + + + 3 + 6 + + 1 + 1 + 20 + + + + + 3 + false + + + + 3 + false + + + + + + 2 + false + + + + 2 + false + + + + 2 + false + + + + - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + 5 + + 1 + 1 + 50 + + + + + 3 + 3 + + 1 + 1 + 20 + + + + + 3 + 200 + + 10 + 10 + 1000 + + + + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + 3 + false + + + + +