diff --git a/CHANGELOG-DEV.md b/CHANGELOG-DEV.md index dd0bf6f..7b38eea 100644 --- a/CHANGELOG-DEV.md +++ b/CHANGELOG-DEV.md @@ -1,3 +1,7 @@ +## 0.1.83.5-dev - 2026-03-15 + +- dev: SerienStream Suche via /suche?term=, Staffel 0 als Filme, Katalog-Suche entfernt + ## 0.1.83.0-dev - 2026-03-15 - dev: Trakt Performance, Suchfilter Phrase-Match, Debug-Settings Expert-Level diff --git a/addon/addon.xml b/addon/addon.xml index f44627f..040451e 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/core/trakt.py b/addon/core/trakt.py index 1fe4bfa..0e00ab8 100644 --- a/addon/core/trakt.py +++ b/addon/core/trakt.py @@ -370,6 +370,40 @@ class TraktClient: return [] return self._parse_history_items(payload) + def get_watched_shows(self, token: str) -> list[TraktItem]: + """GET /users/me/watched/shows – alle Serien mit zuletzt gesehener Episode.""" + status, payload = self._get("/users/me/watched/shows", token=token) + if status != 200 or not isinstance(payload, list): + self._do_log(f"get_watched_shows: status={status}") + return [] + result: list[TraktItem] = [] + for entry in payload: + if not isinstance(entry, dict): + continue + show = entry.get("show") or {} + ids = self._parse_ids((show.get("ids") or {})) + title = str(show.get("title", "") or "") + year = int(show.get("year", 0) or 0) + seasons = entry.get("seasons") or [] + last_season = 0 + last_episode = 0 + for s in seasons: + snum = int((s.get("number") or 0)) + if snum == 0: # Specials überspringen + continue + for ep in (s.get("episodes") or []): + enum = int((ep.get("number") or 0)) + if snum > last_season or (snum == last_season and enum > last_episode): + last_season = snum + last_episode = enum + if title: + result.append(TraktItem( + title=title, year=year, media_type="episode", + ids=ids, season=last_season, episode=last_episode, + )) + self._do_log(f"get_watched_shows: {len(result)} Serien") + return result + # ------------------------------------------------------------------- # Calendar # ------------------------------------------------------------------- diff --git a/addon/default.py b/addon/default.py index eeb453a..d97b994 100644 --- a/addon/default.py +++ b/addon/default.py @@ -2095,8 +2095,17 @@ def _run_async(coro): """Fuehrt eine Coroutine aus, auch wenn Kodi bereits einen Event-Loop hat.""" _ensure_windows_selector_policy() - def _run_with_asyncio_run(): - return asyncio.run(coro) + def _run_without_asyncio_run(): + # asyncio.run() wuerde cancel_all_tasks() aufrufen, was auf Android TV + # wegen eines kaputten _weakrefset.py-Builds zu NameError: 'len' fuehrt. + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(coro) + finally: + try: + loop.close() + except Exception: + pass try: running_loop = asyncio.get_running_loop() @@ -2109,7 +2118,7 @@ def _run_async(coro): def _worker() -> None: try: - result_box["value"] = _run_with_asyncio_run() + result_box["value"] = _run_without_asyncio_run() except BaseException as exc: # pragma: no cover - defensive error_box["error"] = exc @@ -2120,7 +2129,7 @@ def _run_async(coro): raise error_box["error"] return result_box.get("value") - return _run_with_asyncio_run() + return _run_without_asyncio_run() def _series_url_params(plugin: BasisPlugin, title: str) -> dict[str, str]: @@ -4414,13 +4423,15 @@ def _play_episode( preferred_setter([selected_hoster]) try: - link = plugin.stream_link_for(title, season, episode) + with _busy_dialog("Stream wird gesucht..."): + link = plugin.stream_link_for(title, season, episode) if not link: _log("Kein Stream gefunden.", xbmc.LOGWARNING) xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) return _log(f"Stream-Link: {link}", xbmc.LOGDEBUG) - final_link = _resolve_stream_with_retry(plugin, link) + with _busy_dialog("Stream wird aufgelöst..."): + final_link = _resolve_stream_with_retry(plugin, link) if not final_link: return finally: @@ -4815,11 +4826,33 @@ def _show_tag_titles_page(plugin_name: str, tag: str, page: int = 1) -> None: xbmcplugin.endOfDirectory(handle) return titles = [str(t).strip() for t in titles if t and str(t).strip()] - for title in titles: - _add_directory_item(handle, title, "seasons", - {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, - is_folder=True) if titles: + use_source, show_tmdb, prefer_source = _metadata_policy( + plugin_name, plugin, allow_tmdb=_tmdb_list_enabled() + ) + plugin_meta = _collect_plugin_metadata(plugin, titles) if use_source else {} + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + tmdb_titles = list(titles) if show_tmdb else [] + if show_tmdb and prefer_source and use_source: + tmdb_titles = [ + t for t in titles + if _needs_tmdb((plugin_meta.get(t) or ({},))[0], (plugin_meta.get(t) or ({}, {}))[1], + want_plot=show_plot, want_art=show_art) + ] + if show_tmdb and tmdb_titles: + with _busy_dialog("Schlagwort-Liste wird geladen..."): + tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) + for title in titles: + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, []) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) + info_labels = dict(info_labels or {}) + info_labels.setdefault("mediatype", "tvshow") + _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", "tag_titles_page", {"plugin": plugin_name, "tag": tag, "page": str(page + 1)}, is_folder=True) xbmcplugin.endOfDirectory(handle) @@ -4929,7 +4962,7 @@ def _show_trakt_watchlist(media_type: str = "") -> None: _add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True, info_labels=info_labels, art=art) if not items: xbmcgui.Dialog().notification("Trakt", "Watchlist ist leer.", xbmcgui.NOTIFICATION_INFO, 3000) - xbmcplugin.endOfDirectory(handle) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) def _show_trakt_history(page: int = 1) -> None: @@ -4999,7 +5032,7 @@ def _show_trakt_history(page: int = 1) -> None: _add_directory_item(handle, "Naechste Seite >>", "trakt_history", {"page": str(page + 1)}, is_folder=True) if not items and page == 1: xbmcgui.Dialog().notification("Trakt", "Keine History vorhanden.", xbmcgui.NOTIFICATION_INFO, 3000) - xbmcplugin.endOfDirectory(handle) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) def _show_trakt_upcoming() -> None: @@ -5110,7 +5143,7 @@ def _show_trakt_upcoming() -> None: _add_directory_item(handle, label, action, params, is_folder=True, info_labels=info_labels, art=art) - xbmcplugin.endOfDirectory(handle) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) def _show_trakt_continue_watching() -> None: @@ -5127,21 +5160,17 @@ def _show_trakt_continue_watching() -> None: _set_content(handle, "episodes") try: - history = client.get_history(token, media_type="episodes", limit=100) + watched = client.get_watched_shows(token) except Exception as exc: - _log(f"Trakt History fehlgeschlagen: {exc}", xbmc.LOGWARNING) - xbmcgui.Dialog().notification("Trakt", "History konnte nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) + _log(f"Trakt Watched fehlgeschlagen: {exc}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification("Trakt", "Watched-Liste konnte nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcplugin.endOfDirectory(handle) return - # Pro Serie nur den zuletzt gesehenen Eintrag behalten (History ist absteigend sortiert) - seen: dict[str, object] = {} - for item in history: - if item.title and item.title not in seen: - seen[item.title] = item + seen: dict[str, object] = {item.title: item for item in watched if item.title} if not seen: - xbmcgui.Dialog().notification("Trakt", "Keine History vorhanden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcgui.Dialog().notification("Trakt", "Keine gesehenen Serien vorhanden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcplugin.endOfDirectory(handle) return @@ -5169,7 +5198,7 @@ def _show_trakt_continue_watching() -> None: _, art, _ = tmdb_prefetched.get(last.title, ({}, {}, [])) _add_directory_item(handle, display_label, "search", {"query": last.title}, is_folder=True, info_labels=info_labels, art=art) - xbmcplugin.endOfDirectory(handle) + xbmcplugin.endOfDirectory(handle, cacheToDisc=False) # ---------------------------------------------------------------------------