diff --git a/CHANGELOG-NIGHTLY.md b/CHANGELOG-NIGHTLY.md new file mode 100644 index 0000000..a0a96ae --- /dev/null +++ b/CHANGELOG-NIGHTLY.md @@ -0,0 +1,20 @@ +# Changelog (Nightly) + +## 0.1.59-nightly - 2026-02-23 + +- Enthaelt alle Aenderungen aus `0.1.58`. +- Update-Kanal standardmaessig auf `Nightly`. +- Nightly-Repo-URL als Standard gesetzt. +- Settings-Menue neu sortiert: + - Quellen + - Metadaten + - TMDB Erweitert + - Updates + - Debug Global + - Debug Quellen +- Seitengroesse in Listen auf 20 gesetzt. +- `topstream_genre_max_pages` entfernt. + +## Hinweis + +- Nightly ist fuer Tests und kann sich kurzfristig aendern. diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..ce044ae --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,12 @@ +# Changelog (Stable) + +## 0.1.58 - 2026-02-23 + +- Menuebezeichnungen vereinheitlicht (`Haeufig gesehen`, `Neuste Titel`). +- `Neue Titel` und `Neueste Folgen` im Menue zu `Neuste Titel` zusammengelegt. +- Hoster-Header-Anpassung zentral nach `resolve_stream_link` eingebaut. +- Hinweis bei Cloudflare-Block durch ResolveURL statt stiller Fehlversuche. +- Update-Einstellungen erweitert (Kanal, manueller Check, optionaler Auto-Check). +- Metadaten-Parsing in AniWorld und Filmpalast nachgezogen (Cover/Plot robuster). +- Topstreamfilm-Suche: fehlender `urlencode`-Import behoben. +- Einige ungenutzte Funktionen entfernt. diff --git a/addon/addon.xml b/addon/addon.xml index 9bc2d14..f242200 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/default.py b/addon/default.py index db9bc5c..b556ab5 100644 --- a/addon/default.py +++ b/addon/default.py @@ -131,6 +131,7 @@ _TMDB_LOCK = threading.RLock() WATCHED_THRESHOLD = 0.9 POPULAR_MENU_LABEL = "Haeufig gesehen" LATEST_MENU_LABEL = "Neuste Titel" +LIST_PAGE_SIZE = 20 atexit.register(close_all_sessions) @@ -305,10 +306,6 @@ def _playstate_key(*, plugin_name: str, title: str, season: str, episode: str) - return f"{plugin_name}\t{title}\t{season}\t{episode}" -def _playstate_path() -> str: - return _get_log_path("playstate.json") - - def _load_playstate() -> dict[str, dict[str, object]]: return {} @@ -2574,7 +2571,7 @@ def _plugins_with_popular() -> list[tuple[str, BasisPlugin, str]]: def _show_popular(plugin_name: str | None = None, page: int = 1) -> None: handle = _get_handle() - page_size = 10 + page_size = LIST_PAGE_SIZE page = max(1, int(page or 1)) if plugin_name: @@ -2701,7 +2698,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None: def _show_new_titles(plugin_name: str, page: int = 1, *, action_name: str = "new_titles") -> None: handle = _get_handle() - page_size = 10 + page_size = LIST_PAGE_SIZE page = max(1, int(page or 1)) plugin_name = (plugin_name or "").strip() @@ -2949,7 +2946,7 @@ def _show_latest_titles(plugin_name: str, page: int = 1) -> None: def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page: int = 1) -> None: handle = _get_handle() - page_size = 10 + page_size = LIST_PAGE_SIZE page = max(1, int(page or 1)) plugin = _discover_plugins().get(plugin_name) if plugin is None: @@ -3182,23 +3179,28 @@ def _extract_first_int(value: str) -> int | None: return None -def _duration_label(duration_seconds: int) -> str: - try: - duration_seconds = int(duration_seconds or 0) - except Exception: - duration_seconds = 0 - if duration_seconds <= 0: - return "" - total_minutes = max(0, duration_seconds // 60) - hours = max(0, total_minutes // 60) - minutes = max(0, total_minutes % 60) - return f"{hours:02d}:{minutes:02d} Laufzeit" - - def _label_with_duration(label: str, info_labels: dict[str, str] | None) -> str: return label +def _resolveurl_last_error() -> str: + try: + from resolveurl_backend import get_last_error # type: ignore + except Exception: + return "" + try: + return str(get_last_error() or "") + except Exception: + return "" + + +def _is_cloudflare_challenge_error(message: str) -> bool: + text = str(message or "").casefold() + if not text: + return False + return "cloudflare" in text or "challenge" in text or "attention required" in text + + def _play_final_link( link: str, *, @@ -3242,10 +3244,6 @@ def _play_final_link( player.play(item=link, listitem=list_item) -def _track_playback_and_update_state(key: str) -> None: - return - - def _track_playback_and_update_state_async(key: str) -> None: # Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst. return @@ -3334,8 +3332,30 @@ def _play_episode( xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) return _log(f"Stream-Link: {link}", xbmc.LOGDEBUG) - final_link = plugin.resolve_stream_link(link) or link + resolved_link = plugin.resolve_stream_link(link) + if not resolved_link: + err = _resolveurl_last_error() + if _is_cloudflare_challenge_error(err): + _log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification( + "Wiedergabe", + "Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.", + xbmcgui.NOTIFICATION_INFO, + 4500, + ) + return + final_link = resolved_link or link final_link = normalize_resolved_stream_url(final_link, source_url=link) + err = _resolveurl_last_error() + if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip(): + _log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification( + "Wiedergabe", + "Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.", + xbmcgui.NOTIFICATION_INFO, + 4500, + ) + return finally: if restore_hosters is not None and callable(preferred_setter): preferred_setter(restore_hosters) @@ -3422,8 +3442,30 @@ def _play_episode_url( xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) return _log(f"Stream-Link: {link}", xbmc.LOGDEBUG) - final_link = plugin.resolve_stream_link(link) or link + resolved_link = plugin.resolve_stream_link(link) + if not resolved_link: + err = _resolveurl_last_error() + if _is_cloudflare_challenge_error(err): + _log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification( + "Wiedergabe", + "Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.", + xbmcgui.NOTIFICATION_INFO, + 4500, + ) + return + final_link = resolved_link or link final_link = normalize_resolved_stream_url(final_link, source_url=link) + err = _resolveurl_last_error() + if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip(): + _log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification( + "Wiedergabe", + "Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.", + xbmcgui.NOTIFICATION_INFO, + 4500, + ) + return finally: if restore_hosters is not None and callable(preferred_setter): preferred_setter(restore_hosters) diff --git a/addon/plugins/einschalten_plugin.py b/addon/plugins/einschalten_plugin.py index b6aea00..d13e122 100644 --- a/addon/plugins/einschalten_plugin.py +++ b/addon/plugins/einschalten_plugin.py @@ -603,15 +603,6 @@ class EinschaltenPlugin(BasisPlugin): url = urljoin(base + "/", path.lstrip("/")) return f"{url}?{urlencode({'query': query})}" - def _api_movies_url(self, *, with_genres: int, page: int = 1) -> str: - base = self._get_base_url() - if not base: - return "" - params: Dict[str, str] = {"withGenres": str(int(with_genres))} - if page and int(page) > 1: - params["page"] = str(int(page)) - return urljoin(base + "/", "api/movies") + f"?{urlencode(params)}" - def _genre_page_url(self, *, genre_id: int, page: int = 1) -> str: """Genre title pages are rendered server-side and embed the movie list in ng-state. @@ -771,23 +762,6 @@ class EinschaltenPlugin(BasisPlugin): except Exception: return [] - def _fetch_new_titles_movies(self) -> List[MovieItem]: - # "Neue Filme" lives at `/movies/new` and embeds the list in ng-state (`u: "/api/movies"`). - url = self._new_titles_url() - if not url: - return [] - try: - _, body = self._http_get_text(url, timeout=20) - payload = _extract_ng_state_payload(body) - movies = _parse_ng_state_movies(payload) - _log_debug_line(f"parse_ng_state_movies:count={len(movies)}") - if movies: - _log_titles(movies, context="new_titles") - return movies - return [] - except Exception: - return [] - def _fetch_new_titles_movies_page(self, page: int) -> List[MovieItem]: page = max(1, int(page or 1)) url = self._new_titles_url() diff --git a/addon/plugins/topstreamfilm_plugin.py b/addon/plugins/topstreamfilm_plugin.py index ab71fd6..099ff87 100644 --- a/addon/plugins/topstreamfilm_plugin.py +++ b/addon/plugins/topstreamfilm_plugin.py @@ -20,7 +20,7 @@ import os import re import json from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional -from urllib.parse import urljoin +from urllib.parse import urlencode, urljoin try: # pragma: no cover - optional dependency import requests @@ -66,12 +66,9 @@ SETTING_LOG_URLS = "log_urls_topstreamfilm" SETTING_DUMP_HTML = "dump_html_topstreamfilm" SETTING_SHOW_URL_INFO = "show_url_info_topstreamfilm" SETTING_LOG_ERRORS = "log_errors_topstreamfilm" -SETTING_GENRE_MAX_PAGES = "topstream_genre_max_pages" DEFAULT_TIMEOUT = 20 DEFAULT_PREFERRED_HOSTERS = ["supervideo", "dropload", "voe"] MEINECLOUD_HOST = "meinecloud.click" -DEFAULT_GENRE_MAX_PAGES = 20 -HARD_MAX_GENRE_PAGES = 200 HEADERS = { "User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8", @@ -347,22 +344,6 @@ class TopstreamfilmPlugin(BasisPlugin): return urljoin(base if base.endswith("/") else base + "/", href) return href - def _get_setting_bool(self, setting_id: str, *, default: bool = False) -> bool: - return get_setting_bool(ADDON_ID, setting_id, default=default) - - def _get_setting_int(self, setting_id: str, *, default: int) -> int: - if xbmcaddon is None: - return default - try: - addon = xbmcaddon.Addon(ADDON_ID) - getter = getattr(addon, "getSettingInt", None) - if callable(getter): - return int(getter(setting_id)) - raw = str(addon.getSetting(setting_id) or "").strip() - return int(raw) if raw else default - except Exception: - return default - def _notify_url(self, url: str) -> None: notify_url( ADDON_ID, diff --git a/addon/resolveurl_backend.py b/addon/resolveurl_backend.py index 5b9a17a..244c87c 100644 --- a/addon/resolveurl_backend.py +++ b/addon/resolveurl_backend.py @@ -8,8 +8,16 @@ from __future__ import annotations from typing import Optional +_LAST_RESOLVE_ERROR = "" + + +def get_last_error() -> str: + return str(_LAST_RESOLVE_ERROR or "") + def resolve(url: str) -> Optional[str]: + global _LAST_RESOLVE_ERROR + _LAST_RESOLVE_ERROR = "" if not url: return None try: @@ -23,12 +31,14 @@ def resolve(url: str) -> Optional[str]: hmf = hosted(url) valid = getattr(hmf, "valid_url", None) if callable(valid) and not valid(): + _LAST_RESOLVE_ERROR = "invalid url" return None resolver = getattr(hmf, "resolve", None) if callable(resolver): result = resolver() return str(result) if result else None - except Exception: + except Exception as exc: + _LAST_RESOLVE_ERROR = str(exc or "") pass try: @@ -36,8 +46,8 @@ def resolve(url: str) -> Optional[str]: if callable(resolve_fn): result = resolve_fn(url) return str(result) if result else None - except Exception: + except Exception as exc: + _LAST_RESOLVE_ERROR = str(exc or "") return None return None - diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index a3d26e7..d1efbf9 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -1,6 +1,59 @@ - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -8,83 +61,32 @@ - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -