diff --git a/addon/default.py b/addon/default.py index 3976da4..e89a369 100644 --- a/addon/default.py +++ b/addon/default.py @@ -1021,6 +1021,9 @@ def _show_plugin_menu(plugin_name: str) -> None: if _plugin_has_capability(plugin, "alpha"): _add_directory_item(handle, "A-Z", "alpha_index", {"plugin": plugin_name}, is_folder=True) + if _plugin_has_capability(plugin, "series_catalog"): + _add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True) + if _plugin_has_capability(plugin, "popular_series"): _add_directory_item(handle, "Meist gesehen", "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True) @@ -1875,6 +1878,115 @@ def _show_alpha_titles_page(plugin_name: str, letter: str, page: int = 1) -> Non xbmcplugin.endOfDirectory(handle) +def _show_series_catalog(plugin_name: str, page: int = 1) -> None: + handle = _get_handle() + plugin_name = (plugin_name or "").strip() + plugin = _discover_plugins().get(plugin_name) + if plugin is None: + xbmcgui.Dialog().notification("Serien", "Plugin nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + + page = max(1, int(page or 1)) + paging_getter = getattr(plugin, "series_catalog_page", None) + if not callable(paging_getter): + xbmcgui.Dialog().notification("Serien", "Serien nicht verfügbar.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + + total_pages = None + count_getter = getattr(plugin, "series_catalog_page_count", None) + if callable(count_getter): + try: + total_pages = int(count_getter(page) or 1) + except Exception: + total_pages = None + if total_pages is not None: + page = min(page, max(1, total_pages)) + xbmcplugin.setPluginCategory(handle, f"Serien ({page}/{total_pages})") + else: + xbmcplugin.setPluginCategory(handle, f"Serien ({page})") + _set_content(handle, "tvshows") + + if page > 1: + _add_directory_item( + handle, + "Vorherige Seite", + "series_catalog", + {"plugin": plugin_name, "page": str(page - 1)}, + is_folder=True, + ) + + try: + titles = list(paging_getter(page) or []) + except Exception as exc: + _log(f"Serien konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification("Serien", "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + + titles = [str(t).strip() for t in titles if t and str(t).strip()] + titles.sort(key=lambda value: value.casefold()) + + show_tmdb = _get_setting_bool("tmdb_genre_metadata", default=False) + if titles: + if show_tmdb: + with _busy_dialog(): + tmdb_prefetched = _tmdb_labels_and_art_bulk(titles) + for title in titles: + info_labels, art, cast = tmdb_prefetched.get(title, _tmdb_labels_and_art(title)) + info_labels = dict(info_labels or {}) + info_labels.setdefault("mediatype", "tvshow") + if (info_labels.get("mediatype") or "").strip().casefold() == "tvshow": + info_labels.setdefault("tvshowtitle", title) + playstate = _title_playstate(plugin_name, title) + info_labels = _apply_playstate_to_info(dict(info_labels), playstate) + display_label = _label_with_duration(title, info_labels) + display_label = _label_with_playstate(display_label, playstate) + _add_directory_item( + handle, + display_label, + "seasons", + {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, + is_folder=True, + info_labels=info_labels, + art=art, + cast=cast, + ) + else: + for title in titles: + playstate = _title_playstate(plugin_name, title) + _add_directory_item( + handle, + _label_with_playstate(title, playstate), + "seasons", + {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, + is_folder=True, + info_labels=_apply_playstate_to_info({"title": title}, playstate), + ) + + show_next = False + if total_pages is not None: + show_next = page < total_pages + else: + has_more_getter = getattr(plugin, "series_catalog_has_more", None) + if callable(has_more_getter): + try: + show_next = bool(has_more_getter(page)) + except Exception: + show_next = False + + if show_next: + _add_directory_item( + handle, + "Nächste Seite", + "series_catalog", + {"plugin": plugin_name, "page": str(page + 1)}, + is_folder=True, + ) + xbmcplugin.endOfDirectory(handle) + + def _title_group_key(title: str) -> str: raw = (title or "").strip() if not raw: @@ -2862,6 +2974,11 @@ def run() -> None: params.get("letter", ""), _parse_positive_int(params.get("page", "1"), default=1), ) + elif action == "series_catalog": + _show_series_catalog( + params.get("plugin", ""), + _parse_positive_int(params.get("page", "1"), default=1), + ) elif action == "genre_series_group": _show_genre_series_group( params.get("plugin", ""), diff --git a/addon/plugins/filmpalast_plugin.py b/addon/plugins/filmpalast_plugin.py index 9cd11e8..82c6509 100644 --- a/addon/plugins/filmpalast_plugin.py +++ b/addon/plugins/filmpalast_plugin.py @@ -43,6 +43,7 @@ DEFAULT_BASE_URL = "https://filmpalast.to" DEFAULT_TIMEOUT = 20 DEFAULT_PREFERRED_HOSTERS = ["voe", "vidoza", "streamtape", "doodstream", "mixdrop"] SERIES_HINT_PREFIX = "series://filmpalast/" +SERIES_VIEW_PATH = "/serien/view" SEASON_EPISODE_RE = re.compile(r"\bS\s*(\d{1,2})\s*E\s*(\d{1,3})\b", re.IGNORECASE) GLOBAL_SETTING_LOG_URLS = "debug_log_urls" GLOBAL_SETTING_DUMP_HTML = "debug_dump_html" @@ -229,6 +230,7 @@ class FilmpalastPlugin(BasisPlugin): self._genre_page_count_cache: Dict[str, int] = {} self._alpha_to_url: Dict[str, str] = {} self._alpha_page_count_cache: Dict[str, int] = {} + self._series_page_count_cache: Dict[int, int] = {} self._requests_available = REQUESTS_AVAILABLE self._default_preferred_hosters: List[str] = list(DEFAULT_PREFERRED_HOSTERS) self._preferred_hosters: List[str] = list(self._default_preferred_hosters) @@ -497,7 +499,7 @@ class FilmpalastPlugin(BasisPlugin): return max_page def capabilities(self) -> set[str]: - return {"genres", "alpha"} + return {"genres", "alpha", "series_catalog"} def _parse_alpha_links(self, soup: BeautifulSoupT) -> Dict[str, str]: alpha: Dict[str, str] = {} @@ -571,6 +573,45 @@ class FilmpalastPlugin(BasisPlugin): titles.sort(key=lambda value: value.casefold()) return titles + def _series_view_url(self) -> str: + return _absolute_url(SERIES_VIEW_PATH) + + def series_catalog_page_count(self, page: int = 1) -> int: + if not self._requests_available: + return 1 + cache_key = int(page or 1) + if cache_key in self._series_page_count_cache: + return max(1, int(self._series_page_count_cache.get(cache_key, 1))) + base_url = self._series_view_url() + if not base_url: + return 1 + try: + soup = _get_soup(base_url, session=get_requests_session("filmpalast", headers=HEADERS)) + except Exception: + return 1 + pages = self._extract_last_page(soup) + self._series_page_count_cache[cache_key] = max(1, pages) + return self._series_page_count_cache[cache_key] + + def series_catalog_page(self, page: int) -> List[str]: + if not self._requests_available: + return [] + base_url = self._series_view_url() + if not base_url: + return [] + page = max(1, int(page or 1)) + url = base_url if page == 1 else urljoin(base_url.rstrip("/") + "/", f"page/{page}") + try: + soup = _get_soup(url, session=get_requests_session("filmpalast", headers=HEADERS)) + except Exception: + return [] + hits = self._parse_listing_hits(soup) + return self._apply_hits_to_title_index(hits) + + def series_catalog_has_more(self, page: int) -> bool: + total = self.series_catalog_page_count(page) + return page < total + def genres(self) -> List[str]: if not self._requests_available: return []