diff --git a/CHANGELOG-DEV.md b/CHANGELOG-DEV.md index 5f7fc15..993ba43 100644 --- a/CHANGELOG-DEV.md +++ b/CHANGELOG-DEV.md @@ -1,3 +1,7 @@ +## 0.1.87.5-dev - 2026-04-04 + +- dev: bump to 0.1.87.0-dev Changelog-Prozess verbessert + ## 0.1.87.0-dev - 2026-04-04 - dev: bump to 0.1.86.5-dev Trakt-Untermenue, Weiterschauen-Fixes, Scrobbling zuverlässig diff --git a/addon/CHANGELOG-USER.md b/addon/CHANGELOG-USER.md index ecab751..01e9ca8 100644 --- a/addon/CHANGELOG-USER.md +++ b/addon/CHANGELOG-USER.md @@ -1,3 +1,23 @@ +## 0.1.87.5 + +**TMDb Helper Integration** +- Einstellungen → Tools: Player-Dateien für TMDb Helper direkt aus ViewIT installierbar +- Zwei Player mitgeliefert: SerienStream (direkte Episodenwiedergabe) und Globale Suche +- Menüpunkt ist ausgegraut wenn TMDb Helper nicht installiert ist + +**Metadaten / Poster** +- Poster und Infos werden jetzt korrekt geladen wenn kein eigener TMDb-API-Key eingetragen ist (Fallback auf installierten Kodi-Scraper oder Community-Key war nicht aktiv) +- „Neueste Episoden" (SerienStream, Aniworld) zeigt jetzt Poster und Serieninfos + +**Topstreamfilm** +- „Neueste Titel" entfernt – das Feature existiert auf der aktuellen Domain nicht + +**Filmpalast** +- Hoster „Veev HD" wird jetzt erkannt und in der Hoster-Liste angezeigt + +**Kosmetik** +- Menü-Icons für Hauptmenü, Plugin-Menüs und Trakt-Untermenü + ## 0.1.87.0 **Trakt** diff --git a/addon/addon.xml b/addon/addon.xml index 464e82f..7817471 100644 --- a/addon/addon.xml +++ b/addon/addon.xml @@ -1,5 +1,5 @@ - + diff --git a/addon/default.py b/addon/default.py index 4854c8a..2a28b71 100644 --- a/addon/default.py +++ b/addon/default.py @@ -117,11 +117,25 @@ from metadata_utils import ( needs_tmdb as _needs_tmdb, ) from tmdb import TmdbCastMember, TmdbExternalIds, fetch_external_ids, fetch_tv_episode_credits, lookup_movie, lookup_tv_season, lookup_tv_season_summary, lookup_tv_show +from core.metadata import _resolve_tmdb_api_key from core.router import Router _router = Router() PLUGIN_DIR = Path(__file__).with_name("plugins") +_PICTURES_DIR = Path(__file__).parent / "pictures" +_MENU_ICONS: dict[str, str] = { + "search": str(_PICTURES_DIR / "suche.png"), + "plugin_search": str(_PICTURES_DIR / "suche.png"), + "genres": str(_PICTURES_DIR / "generes.png"), + "alpha_index": str(_PICTURES_DIR / "a_z.png"), + "series_catalog": str(_PICTURES_DIR / "serien.png"), + "settings": str(_PICTURES_DIR / "einstellung.png"), + "trakt_menu": str(_PICTURES_DIR / "seriensammlung.png"), + "trakt_continue": str(_PICTURES_DIR / "hgesehn.png"), + "latest_titles": str(_PICTURES_DIR / "neuste.png"), +} + _PLUGIN_CACHE: dict[str, BasisPlugin] | None = None _TMDB_CACHE: dict[str, tuple[dict[str, str], dict[str, str]]] = {} _TMDB_CAST_CACHE: dict[str, list[TmdbCastMember]] = {} @@ -179,7 +193,7 @@ def _fetch_and_cache_imdb_id(title_key: str, tmdb_id: int, kind: str) -> str: cached = _tmdb_cache_get(_IMDB_ID_CACHE, title_key) if cached is not None: return cached - api_key = _get_setting_string("tmdb_api_key").strip() + api_key = _resolve_tmdb_api_key(_get_setting_string("tmdb_api_key").strip()) if not api_key or api_key == "None" or not tmdb_id: return "" ext = fetch_external_ids(kind=kind, tmdb_id=tmdb_id, api_key=api_key) @@ -870,7 +884,7 @@ def _tmdb_labels_and_art(title: str) -> tuple[dict[str, str], dict[str, str], li art: dict[str, str] = {} cast: list[TmdbCastMember] = [] query = (title or "").strip() - api_key = _get_setting_string("tmdb_api_key").strip() + api_key = _resolve_tmdb_api_key(_get_setting_string("tmdb_api_key").strip()) log_requests = _get_setting_bool("tmdb_log_requests", default=False) log_responses = _get_setting_bool("tmdb_log_responses", default=False) if api_key and api_key != "None": @@ -1020,7 +1034,7 @@ def _tmdb_episode_labels_and_art(*, title: str, season_label: str, episode_label season_key = (tmdb_id, season_number, language, flags) cached_season = _tmdb_cache_get(_TMDB_SEASON_CACHE, season_key) if cached_season is None: - api_key = _get_setting_string("tmdb_api_key").strip() + api_key = _resolve_tmdb_api_key(_get_setting_string("tmdb_api_key").strip()) if not api_key or api_key == "None": return _trakt_episode_labels_and_art( title=title, season_label=season_label, episode_label=episode_label @@ -1151,7 +1165,7 @@ def _tmdb_episode_cast(*, title: str, season_label: str, episode_label: str) -> if cached is not None: return list(cached) - api_key = _get_setting_string("tmdb_api_key").strip() + api_key = _resolve_tmdb_api_key(_get_setting_string("tmdb_api_key").strip()) if not api_key or api_key == "None": _tmdb_cache_set(_TMDB_EPISODE_CAST_CACHE, cache_key, []) return [] @@ -1179,6 +1193,11 @@ def _tmdb_episode_cast(*, title: str, season_label: str, episode_label: str) -> return list(cast) +def _icon_for(action: str) -> dict[str, str] | None: + path = _MENU_ICONS.get(action) + return {"icon": path, "thumb": path} if path else None + + def _add_directory_item( handle: int, label: str, @@ -1238,6 +1257,8 @@ UPDATE_ADDON_ID = "plugin.video.viewit" RESOLVEURL_ADDON_ID = "script.module.resolveurl" RESOLVEURL_AUTO_INSTALL_INTERVAL_SEC = 6 * 60 * 60 YTDLP_ADDON_ID = "script.module.yt-dlp" +TMDB_HELPER_ADDON_ID = "plugin.video.themoviedb.helper" +TMDB_HELPER_PLAYERS_DIR = Path.home() / ".kodi" / "addons" / TMDB_HELPER_ADDON_ID / "resources" / "players" def _selected_update_channel() -> int: @@ -1716,7 +1737,6 @@ def _maybe_auto_install_resolveurl(action: str | None) -> None: def _sync_tmdb_active_key_setting() -> None: - from core.metadata import _resolve_tmdb_api_key raw_key = _get_setting_string("tmdb_api_key").strip() active_key = _resolve_tmdb_api_key(raw_key) if active_key: @@ -1731,6 +1751,34 @@ def _sync_ytdlp_status_setting() -> None: _set_setting_string("ytdlp_status", status) +def _sync_tmdb_helper_status_setting() -> None: + status = "Installiert" if _is_addon_installed(TMDB_HELPER_ADDON_ID) else "Nicht installiert" + _set_setting_string("tmdb_helper_status", status) + + +_TMDB_HELPER_PLAYERS_SOURCE = Path(__file__).parent / "resources" / "players" + + +def _install_tmdb_helper_players() -> None: + if not _is_addon_installed(TMDB_HELPER_ADDON_ID): + xbmcgui.Dialog().notification("TMDb Helper", "TMDb Helper ist nicht installiert.", xbmcgui.NOTIFICATION_WARNING, 4000) + return + try: + source_files = list(_TMDB_HELPER_PLAYERS_SOURCE.glob("*.json")) + if not source_files: + xbmcgui.Dialog().notification("TMDb Helper", "Keine Player-Dateien gefunden.", xbmcgui.NOTIFICATION_WARNING, 4000) + return + TMDB_HELPER_PLAYERS_DIR.mkdir(parents=True, exist_ok=True) + import shutil + for src in source_files: + shutil.copy2(src, TMDB_HELPER_PLAYERS_DIR / src.name) + _log(f"TMDb Helper Player installiert: {[f.name for f in source_files]}") + xbmcgui.Dialog().notification("TMDb Helper", f"{len(source_files)} Player installiert.", xbmcgui.NOTIFICATION_INFO, 3000) + except Exception as exc: + _log(f"TMDb Helper Player Installation fehlgeschlagen: {exc}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification("TMDb Helper", f"Fehler: {exc}", xbmcgui.NOTIFICATION_ERROR, 5000) + + def _fetch_ytdlp_zip_url() -> str: """Ermittelt die aktuellste ZIP-URL fuer script.module.yt-dlp von GitHub.""" api_url = "https://api.github.com/repos/lekma/script.module.yt-dlp/releases/latest" @@ -1827,6 +1875,7 @@ def _sync_update_version_settings() -> None: _set_setting_string("update_installed_version", addon_version) _sync_resolveurl_status_setting() _sync_ytdlp_status_setting() + _sync_tmdb_helper_status_setting() _sync_update_channel_status_settings() _sync_tmdb_active_key_setting() @@ -1845,7 +1894,7 @@ def _show_root_menu() -> None: "select_update_version", ) - _add_directory_item(handle, "Suche in allen Quellen", "search") + _add_directory_item(handle, "Suche in allen Quellen", "search", art=_icon_for("search")) plugins = _discover_plugins() for plugin_name in sorted(plugins.keys(), key=lambda value: value.casefold()): @@ -1853,9 +1902,9 @@ def _show_root_menu() -> None: # Trakt-Menue (nur wenn aktiviert) if _get_setting_bool("trakt_enabled", default=False): - _add_directory_item(handle, "Trakt", "trakt_menu", is_folder=True) + _add_directory_item(handle, "Trakt", "trakt_menu", is_folder=True, art=_icon_for("trakt_menu")) - _add_directory_item(handle, "Einstellungen", "settings") + _add_directory_item(handle, "Einstellungen", "settings", art=_icon_for("settings")) xbmcplugin.endOfDirectory(handle) @@ -1863,7 +1912,7 @@ def _show_trakt_menu() -> None: handle = _get_handle() xbmcplugin.setPluginCategory(handle, "Trakt") if _trakt_load_token(): - _add_directory_item(handle, "Weiterschauen", "trakt_continue", is_folder=True) + _add_directory_item(handle, "Weiterschauen", "trakt_continue", is_folder=True, art=_icon_for("trakt_continue")) _add_directory_item(handle, "Trakt Upcoming", "trakt_upcoming", is_folder=True) _add_directory_item(handle, "Trakt Watchlist", "trakt_watchlist", is_folder=True) _add_directory_item(handle, "Trakt History", "trakt_history", {"page": "1"}, is_folder=True) @@ -1884,19 +1933,19 @@ def _show_plugin_menu(plugin_name: str) -> None: xbmcplugin.setPluginCategory(handle, plugin_name) if callable(getattr(plugin, "search_titles", None)): - _add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True) + _add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True, art=_icon_for("plugin_search")) if _plugin_has_capability(plugin, "new_titles") or _plugin_has_capability(plugin, "latest_episodes"): - _add_directory_item(handle, LATEST_MENU_LABEL, "latest_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True) + _add_directory_item(handle, LATEST_MENU_LABEL, "latest_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True, art=_icon_for("latest_titles")) if _plugin_has_capability(plugin, "genres"): - _add_directory_item(handle, "Genres", "genres", {"plugin": plugin_name}, is_folder=True) + _add_directory_item(handle, "Genres", "genres", {"plugin": plugin_name}, is_folder=True, art=_icon_for("genres")) if _plugin_has_capability(plugin, "alpha"): - _add_directory_item(handle, "A-Z", "alpha_index", {"plugin": plugin_name}, is_folder=True) + _add_directory_item(handle, "A-Z", "alpha_index", {"plugin": plugin_name}, is_folder=True, art=_icon_for("alpha_index")) if _plugin_has_capability(plugin, "series_catalog"): - _add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True) + _add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True, art=_icon_for("series_catalog")) if _plugin_has_capability(plugin, "popular_series"): _add_directory_item(handle, POPULAR_MENU_LABEL, "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True) @@ -1919,7 +1968,7 @@ def _show_plugin_menu(plugin_name: str) -> None: xbmcplugin.endOfDirectory(handle) -def _show_plugin_search(plugin_name: str) -> None: +def _show_plugin_search(plugin_name: str, query: str = "") -> None: plugin_name = (plugin_name or "").strip() plugin = _discover_plugins().get(plugin_name) if not plugin: @@ -1927,13 +1976,15 @@ def _show_plugin_search(plugin_name: str) -> None: _show_root_menu() return - _log(f"Plugin-Suche gestartet: {plugin_name}") - dialog = xbmcgui.Dialog() - query = dialog.input(f"{plugin_name}: Titel eingeben", type=xbmcgui.INPUT_ALPHANUM).strip() + query = (query or "").strip() if not query: - _log("Plugin-Suche abgebrochen (leere Eingabe).", xbmc.LOGDEBUG) - _show_plugin_menu(plugin_name) - return + _log(f"Plugin-Suche gestartet: {plugin_name}") + dialog = xbmcgui.Dialog() + query = dialog.input(f"{plugin_name}: Titel eingeben", type=xbmcgui.INPUT_ALPHANUM).strip() + if not query: + _log("Plugin-Suche abgebrochen (leere Eingabe).", xbmc.LOGDEBUG) + _show_plugin_menu(plugin_name) + return _log(f"Plugin-Suchbegriff ({plugin_name}): {query}", xbmc.LOGDEBUG) _show_plugin_search_results(plugin_name, query) @@ -2556,7 +2607,7 @@ def _show_seasons(plugin_name: str, title: str, series_url: str = "") -> None: # Staffel-Metadaten (Plot/Poster) optional via TMDB. if show_tmdb: _tmdb_labels_and_art(title) - api_key = _get_setting_string("tmdb_api_key").strip() if show_tmdb else "" + api_key = _resolve_tmdb_api_key(_get_setting_string("tmdb_api_key").strip()) if show_tmdb else "" language = _get_setting_string("tmdb_language").strip() or "de-DE" show_plot = _get_setting_bool("tmdb_show_plot", default=True) show_art = _get_setting_bool("tmdb_show_art", default=True) @@ -3544,6 +3595,8 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None: xbmcplugin.endOfDirectory(handle) return + # Unique Serientitel für Metadaten-Prefetch sammeln + valid_entries = [] for entry in entries: try: title = str(getattr(entry, "series_title", "") or "").strip() @@ -3555,7 +3608,28 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None: continue if not title or not url or season_number < 0 or episode_number <= 0: continue + valid_entries.append((title, season_number, episode_number, url, airdate)) + unique_titles = list(dict.fromkeys(t for t, *_ in valid_entries)) + use_source, show_tmdb, prefer_source = _metadata_policy( + plugin_name, plugin, allow_tmdb=_tmdb_enabled() + ) + plugin_meta = _collect_plugin_metadata(plugin, unique_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(unique_titles) if show_tmdb else [] + if show_tmdb and prefer_source and use_source: + tmdb_titles = [t for t in unique_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(f"{LATEST_MENU_LABEL} wird geladen..."): + tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) + + for title, season_number, episode_number, url, airdate in valid_entries: season_label = f"Staffel {season_number}" episode_label = f"Episode {episode_number}" key = _playstate_key(plugin_name=plugin_name, title=title, season=season_label, episode=episode_label) @@ -3564,13 +3638,20 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None: label = f"{title} - S{season_number:02d}E{episode_number:02d}" label = _label_with_playstate(label, playstate) - info_labels: dict[str, object] = { + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, []) + meta = plugin_meta.get(title) + merged_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) + + info_labels: dict[str, object] = dict(merged_labels or {}) + info_labels.update({ "title": f"{title} - S{season_number:02d}E{episode_number:02d}", "tvshowtitle": title, "season": season_number, "episode": episode_number, "mediatype": "episode", - } + }) + if airdate: + info_labels.setdefault("aired", airdate) info_labels = _apply_playstate_to_info(info_labels, playstate) _add_directory_item( @@ -3586,6 +3667,8 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None: }, is_folder=False, info_labels=info_labels, + art=art, + cast=cast, ) has_more_fn = getattr(plugin, "latest_episodes_has_more", None) @@ -5332,7 +5415,7 @@ def _route_plugin_menu(params: dict[str, str]) -> None: @_router.route("plugin_search") def _route_plugin_search(params: dict[str, str]) -> None: - _show_plugin_search(params.get("plugin", "")) + _show_plugin_search(params.get("plugin", ""), params.get("query", "")) @_router.route("genres") @@ -5430,6 +5513,11 @@ def _route_install_ytdlp(params: dict[str, str]) -> None: _ensure_ytdlp_installed(force=True, silent=False) +@_router.route("install_tmdb_helper_players") +def _route_install_tmdb_helper_players(params: dict[str, str]) -> None: + _install_tmdb_helper_players() + + @_router.route("choose_source") def _route_choose_source(params: dict[str, str]) -> None: _show_choose_source(params.get("title", ""), params.get("plugins", "")) @@ -5480,6 +5568,63 @@ def _route_play_movie(params: dict[str, str]) -> None: _play_episode(plugin_name, title, "Film", "Stream", resolve_handle=_get_handle()) +@_router.route("play_direct") +def _route_play_direct(params: dict[str, str]) -> None: + """Direkte Wiedergabe ohne Ordner-Navigation: sucht Titel im Plugin, nimmt besten Match.""" + plugin_name = (params.get("plugin", "") or "").strip() + query = (params.get("query", "") or "").strip() + season_num = _parse_positive_int(params.get("season", "1"), default=1) + episode_num = _parse_positive_int(params.get("episode", "1"), default=1) + is_movie = params.get("type", "").lower() == "movie" + + plugin = _discover_plugins().get(plugin_name) + if not plugin: + xbmcgui.Dialog().notification("ViewIT", f"Plugin nicht gefunden: {plugin_name}", xbmcgui.NOTIFICATION_ERROR, 4000) + return + if not query: + xbmcgui.Dialog().notification("ViewIT", "Kein Suchtitel angegeben.", xbmcgui.NOTIFICATION_ERROR, 4000) + return + + # Suche im Plugin + try: + search_coro = _call_plugin_search(plugin, query) + results = _run_async(search_coro) + results = _clean_search_titles([str(t).strip() for t in (results or []) if t and str(t).strip()]) + except Exception as exc: + xbmcgui.Dialog().notification("ViewIT", f"Suche fehlgeschlagen: {exc}", xbmcgui.NOTIFICATION_ERROR, 4000) + return + + if not results: + xbmcgui.Dialog().notification("ViewIT", f"Kein Ergebnis für: {query}", xbmcgui.NOTIFICATION_WARNING, 4000) + return + + # Besten Match wählen: bevorzuge Titel die den Query enthalten + from search_utils import matches_query as _mq + matched = [r for r in results if _mq(query, title=r)] + title = matched[0] if matched else results[0] + + if is_movie: + _play_episode(plugin_name, title, "Film", "Stream", resolve_handle=_get_handle()) + else: + season_label = f"Staffel {season_num}" + # Episodenlabel exakt aus dem Plugin holen (z.B. "Episode 3: Titel (Original)") + episode_label = f"Episode {episode_num}" + episodes = getattr(plugin, "episodes_for", None) + if callable(episodes): + try: + ep_list = episodes(title, season_label) + # Suche das Label das mit "Episode {episode_num}:" oder "Episode {episode_num} " beginnt + prefix = f"Episode {episode_num}:" + prefix2 = f"Episode {episode_num} " + for ep in (ep_list or []): + if ep.startswith(prefix) or ep.startswith(prefix2) or ep == f"Episode {episode_num}": + episode_label = ep + break + except Exception: + pass + _play_episode(plugin_name, title, season_label, episode_label, resolve_handle=_get_handle()) + + @_router.route("play_episode_url") def _route_play_episode_url(params: dict[str, str]) -> None: _play_episode_url( diff --git a/addon/pictures/Filme.png b/addon/pictures/Filme.png new file mode 100644 index 0000000..1631517 Binary files /dev/null and b/addon/pictures/Filme.png differ diff --git a/addon/pictures/a_z.png b/addon/pictures/a_z.png new file mode 100644 index 0000000..a6c7fe5 Binary files /dev/null and b/addon/pictures/a_z.png differ diff --git a/addon/pictures/einstellung.png b/addon/pictures/einstellung.png new file mode 100644 index 0000000..04686b6 Binary files /dev/null and b/addon/pictures/einstellung.png differ diff --git a/addon/pictures/generes.png b/addon/pictures/generes.png new file mode 100644 index 0000000..d1a3d93 Binary files /dev/null and b/addon/pictures/generes.png differ diff --git a/addon/pictures/hgesehn.png b/addon/pictures/hgesehn.png new file mode 100644 index 0000000..055bc5c Binary files /dev/null and b/addon/pictures/hgesehn.png differ diff --git a/addon/pictures/neuste.png b/addon/pictures/neuste.png new file mode 100644 index 0000000..5552f9d Binary files /dev/null and b/addon/pictures/neuste.png differ diff --git a/addon/pictures/serien.png b/addon/pictures/serien.png new file mode 100644 index 0000000..7b51d48 Binary files /dev/null and b/addon/pictures/serien.png differ diff --git a/addon/pictures/seriensammlung.png b/addon/pictures/seriensammlung.png new file mode 100644 index 0000000..be387c1 Binary files /dev/null and b/addon/pictures/seriensammlung.png differ diff --git a/addon/pictures/suche.png b/addon/pictures/suche.png new file mode 100644 index 0000000..1e7c9c3 Binary files /dev/null and b/addon/pictures/suche.png differ diff --git a/addon/plugins/filmpalast_plugin.py b/addon/plugins/filmpalast_plugin.py index 967b4f6..0ab1c95 100644 --- a/addon/plugins/filmpalast_plugin.py +++ b/addon/plugins/filmpalast_plugin.py @@ -893,8 +893,8 @@ class FilmpalastPlugin(BasisPlugin): for block in soup.select("ul.currentStreamLinks"): host_name_node = block.select_one("li.hostBg .hostName") host_name = self._normalize_hoster_name(host_name_node.get_text(" ", strip=True) if host_name_node else "") - play_anchor = block.select_one("li.streamPlayBtn a[href], a.button.iconPlay[href]") - href = (play_anchor.get("href") if play_anchor else "") or "" + play_anchor = block.select_one("li.streamPlayBtn a[href], li.streamPlayBtn a[data-player-url], a.button.iconPlay[href]") + href = (play_anchor.get("href") or play_anchor.get("data-player-url") if play_anchor else "") or "" play_url = _absolute_url(href).strip() if not play_url: continue diff --git a/addon/plugins/topstreamfilm_plugin.py b/addon/plugins/topstreamfilm_plugin.py index bafddbe..d097e2a 100644 --- a/addon/plugins/topstreamfilm_plugin.py +++ b/addon/plugins/topstreamfilm_plugin.py @@ -1167,7 +1167,7 @@ class TopstreamfilmPlugin(BasisPlugin): return resolve_via_resolveurl(link, fallback_to_link=True) def capabilities(self) -> set[str]: - return {"genres", "popular_series", "year_filter", "new_titles"} + return {"genres", "popular_series", "year_filter"} def years_available(self) -> List[str]: """Liefert verfügbare Erscheinungsjahre (aktuelles Jahr bis 1980).""" diff --git a/addon/resources/players/viewit_global.json b/addon/resources/players/viewit_global.json new file mode 100644 index 0000000..480d1d8 --- /dev/null +++ b/addon/resources/players/viewit_global.json @@ -0,0 +1,11 @@ +{ + "name" : "ViewIT (Globale Suche)", + "plugin" : "plugin.video.viewit", + "priority" : 900, + "assert" : { + "search_movie": ["title"], + "search_episode": ["showname"] + }, + "search_movie" : "plugin://plugin.video.viewit/?action=search&query={title_url}", + "search_episode" : "plugin://plugin.video.viewit/?action=search&query={showname_url}" +} diff --git a/addon/resources/players/viewit_serienstream.json b/addon/resources/players/viewit_serienstream.json new file mode 100644 index 0000000..dfc1dcc --- /dev/null +++ b/addon/resources/players/viewit_serienstream.json @@ -0,0 +1,21 @@ +{ + "name" : "ViewIT (SerienStream)", + "plugin" : "plugin.video.viewit", + "priority" : 1000, + "provider" : "SerienStream", + "is_resolvable" : "true", + "assert" : { + "play_movie": ["title"], + "play_episode": ["showname", "season", "episode"], + "search_movie": ["title"], + "search_episode": ["showname"] + }, + "play_movie" : "plugin://plugin.video.viewit/?action=play_direct&plugin=Serienstream&type=movie&query={title_url}", + "play_episode" : "plugin://plugin.video.viewit/?action=play_direct&plugin=Serienstream&type=episode&query={showname_url}&season={season}&episode={episode}", + "search_movie" : [ + "plugin://plugin.video.viewit/?action=plugin_search&plugin=Serienstream&query={title_url}" + ], + "search_episode" : [ + "plugin://plugin.video.viewit/?action=plugin_search&plugin=Serienstream&query={showname_url}" + ] +} diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index caa3cfd..3ccb24b 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -141,6 +141,11 @@ + + + + + diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index a623763..04fef84 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -58,6 +58,26 @@ Es ergänzt die Detaildokumente `DEFAULT_ROUTER.md` und `PLUGIN_SYSTEM.md`. - Zentrale Sammlung wiederverwendeter Regulärer Ausdrücke (Staffel/Episoden‑Tags, Ziffern etc.). - Ziel: Konsistenz und Vermeidung von fehleranfälligem Copy/Paste in Plugins. +- **yt-dlp Helper (`addon/ytdlp_helper.py`)** + - Kapselt Zugriffe auf `yt-dlp` zum Auflösen von Stream-URLs. + - Wird als optionales Backend für Hoster genutzt, die weder ResolveURL noch direkte Links liefern. + +- **Genre-Helfer (`addon/genre_utils.py`)** + - Hilfsfunktionen für Genre-Normalisierung und -Mapping (plugin-übergreifend wiederverwendbar). + +- **Such-Helfer (`addon/search_utils.py`)** + - Gemeinsame Logik für titelbasierte Volltextsuche (Wortmatch, Normalisierung). + - Wird vom Router genutzt, um Plugin-Suchtreffer konsistent zu filtern. + +- **Kern-Module (`addon/core/`)** + - `trakt.py` – Trakt.tv-Integration (OAuth, Scrobbling, Watchlist, Upcoming, History). + - `metadata.py` – Metadaten-Aggregation aus Plugins und TMDB. + - `gui.py` – Dialog-Helfer und UI-Utilities (z.B. Changelog-Dialog). + - `playstate.py` – Playstate-Hilfsfunktionen (Schlüssel­berechnung, Zustandsabfrage). Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst. + - `plugin_manager.py` – Plugin-Discovery und -Instanziierung. + - `router.py` – Routing-Helfer und Aktions-Dispatch. + - `updater.py` – Versionsprüfung und Addon-Update-Flow. + - **Plugins (`addon/plugins/*.py`)** - Konkrete Integrationen zu einzelnen Providern (z.B. Serien-/Filmportale). - Implementieren `BasisPlugin` und optional zusätzliche Capabilities. diff --git a/docs/DEFAULT_ROUTER.md b/docs/DEFAULT_ROUTER.md index 4503d58..c6a1f22 100644 --- a/docs/DEFAULT_ROUTER.md +++ b/docs/DEFAULT_ROUTER.md @@ -27,9 +27,10 @@ Typische Aktionen: - `play_episode_url` ## Playstate -- Speicherort: Addon Profilordner, Datei `playstate.json` -- Key: Plugin + Titel + Staffel + Episode -- Werte: watched, playcount, resume_position, resume_total +Eigenes Resume/Watched-Tracking ist deaktiviert (`addon/core/playstate.py`). +Kodi verwaltet den Playstate vollständig selbst (Watched-Status, Resume-Position). +Die Helfer-Funktionen in `playstate.py` (Schlüsselberechnung, Zustandsabfrage) sind +noch vorhanden, aber `track_playback_and_update_state_async()` ist ein No-op. ## Wichtige Helper - Plugin Loader und Discovery diff --git a/docs/PLUGIN_DEVELOPMENT.md b/docs/PLUGIN_DEVELOPMENT.md index e5f2607..568ec2c 100644 --- a/docs/PLUGIN_DEVELOPMENT.md +++ b/docs/PLUGIN_DEVELOPMENT.md @@ -19,9 +19,15 @@ Jedes Plugin implementiert: - `genres()` - `popular_series()` - `latest_episodes(page: int = 1)` +- `latest_titles(page: int = 1)` - `titles_for_genre(genre: str)` - `titles_for_genre_page(genre: str, page: int)` - `titles_for_genre_group_page(...)` / `genre_has_more(...)` (Paging / Alphabet-Gruppen) +- `years_available()` / `titles_for_year(year, page)` +- `countries_available()` / `titles_for_country(country, page)` +- `collections()` / `titles_for_collection(collection, page)` +- `tags()` / `titles_for_tag(tag, page)` +- `random_title()` - `stream_link_for(...)` - `stream_link_for_url(...)` - `available_hosters_for(...)` @@ -39,12 +45,17 @@ Wenn keine echten Staffeln existieren: ## Capabilities Ein Plugin kann Features melden ueber `capabilities()`. Bekannte Werte: -- `popular_series` -- `genres` -- `latest_episodes` -- `new_titles` -- `alpha` -- `series_catalog` +- `popular_series` – beliebte Serien/Filme verfügbar +- `genres` – Genre-Navigation +- `latest_episodes` – neu erschienene Episoden (`latest_episodes(page)`) +- `new_titles` – neu hinzugefügte Titel (`latest_titles(page)`) +- `alpha` – alphabetische Navigation +- `series_catalog` – vollständiger Serienindex +- `year_filter` – Filter nach Erscheinungsjahr (`years_available()`, `titles_for_year()`) +- `country_filter` – Filter nach Produktionsland (`countries_available()`, `titles_for_country()`) +- `collections` – Sammlungen/Filmreihen (`collections()`, `titles_for_collection()`) +- `tags` – Schlagwort-Suche (`tags()`, `titles_for_tag()`) +- `random` – zufälliger Titel (`random_title()`) ## Suche Aktuelle Regeln fuer Suchtreffer: @@ -58,6 +69,11 @@ Siehe als Referenz: - `addon/plugins/serienstream_plugin.py` - `addon/plugins/aniworld_plugin.py` - `addon/plugins/topstreamfilm_plugin.py` +- `addon/plugins/hdfilme_plugin.py` +- `addon/plugins/kkiste_plugin.py` +- `addon/plugins/moflix_plugin.py` +- `addon/plugins/netzkino_plugin.py` +- `addon/plugins/youtube_plugin.py` ## Settings Pro Plugin meist `*_base_url`. @@ -68,6 +84,10 @@ Beispiele: - `topstream_base_url` - `filmpalast_base_url` - `doku_streams_base_url` +- `hdfilme_base_url` +- `kkiste_base_url` +- `moflix_base_url` +- `netzkino_base_url` ## Playback Flow 1. Episode oder Film auswaehlen. diff --git a/docs/PLUGIN_SYSTEM.md b/docs/PLUGIN_SYSTEM.md index 37394ac..00fa930 100644 --- a/docs/PLUGIN_SYSTEM.md +++ b/docs/PLUGIN_SYSTEM.md @@ -19,6 +19,11 @@ Relevante Dateien: - `aniworld_plugin.py` - `filmpalast_plugin.py` - `dokustreams_plugin.py` +- `hdfilme_plugin.py` +- `kkiste_plugin.py` +- `moflix_plugin.py` +- `netzkino_plugin.py` +- `youtube_plugin.py` - `_template_plugin.py` (Vorlage) ## Discovery Ablauf @@ -41,12 +46,17 @@ Weitere Methoden sind optional und werden nur genutzt, wenn vorhanden. ## Capabilities Plugins koennen Features aktiv melden. Typische Werte: -- `popular_series` -- `genres` -- `latest_episodes` -- `new_titles` -- `alpha` -- `series_catalog` +- `popular_series` – beliebte Serien/Filme +- `genres` – Genre-Navigation +- `latest_episodes` – neu erschienene Episoden +- `new_titles` – neu hinzugefügte Titel (einfache Stringliste) +- `alpha` – alphabetische Navigation +- `series_catalog` – vollständiger Serienindex +- `year_filter` – Filter nach Erscheinungsjahr +- `country_filter` – Filter nach Produktionsland +- `collections` – Sammlungen/Filmreihen +- `tags` – Schlagwort-Suche +- `random` – zufälliger Titel Das UI zeigt nur Menues fuer aktiv gemeldete Features.