From 6ce1bf71c1febb8e92e42eb7618c47109c77d577 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Sat, 7 Feb 2026 16:11:48 +0100 Subject: [PATCH] Add Doku-Streams plugin and prefer source metadata --- .../plugin_interface.cpython-312.pyc | Bin 2909 -> 0 bytes addon/default.py | 432 +++++++++++++--- .../aniworld_plugin.cpython-312.pyc | Bin 64688 -> 0 bytes .../topstreamfilm_plugin.cpython-312.pyc | Bin 53762 -> 0 bytes addon/plugins/dokustreams_plugin.py | 476 ++++++++++++++++++ addon/resources/settings.xml | 4 + 6 files changed, 829 insertions(+), 83 deletions(-) delete mode 100644 addon/__pycache__/plugin_interface.cpython-312.pyc delete mode 100644 addon/plugins/__pycache__/aniworld_plugin.cpython-312.pyc delete mode 100644 addon/plugins/__pycache__/topstreamfilm_plugin.cpython-312.pyc create mode 100644 addon/plugins/dokustreams_plugin.py diff --git a/addon/__pycache__/plugin_interface.cpython-312.pyc b/addon/__pycache__/plugin_interface.cpython-312.pyc deleted file mode 100644 index 89a7f67d4f720d997eff020505c6b523a03b9a16..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2909 zcmaJ@OOM-B6uyof=RGscOk4VZ8cIu}YLbGwfT4gom8hx?Lq*z65ON*gn~W7d(tDj& z<5gcsmDsSGJ-ezx`~cXoL_%VTHxwZR>?%?um=)(-J2TE?z>9OQ&$*B9eCJ_*SF4o? zJl}t9I$!97{DLo&C!b7|@j6Vl2qhgt6`FGuFW1Q_I9FZO(>j`$@8spX=IUOdQ}BwN zqF3sa6p|yC3C%wwRA<@~H94z)V~#H@j~Q)y!*{|^gv@oB(Hk*tTn(tR ze9ay8VM+53+}Tj#isk!3XoXJTi$*S1FRosS=d7*>xn+kQ3pWDVP-AV)5n){XaER+H zH&)v$w3E9NnF3M9_u+kuFw#-Lx!etKsiV>y)2Mm@EG$pwsP<5MlJ4#3pqU4Eb~Odi zEKoh8DbfS9@KEcNXq^^eEYn3=g0VuEXc@*TJxD7s)<9o{ac;BLI22b_E#Zj7-Kcp{ zdNc@`A=Lxd5+bF=LV~XNA?tHV1M{1)3jW8M4@BCz6eU)*RMjJTxEz%s{@Ffk$)WI%hqwBtf7K#Aak~u;%(>0)%UVxxF}l3j(w$@O;DA zzSi0ZJl3)yUVgvT4o2K&t=kT}#;G3KcgO*6FyXw|ZM%Z+~Bi^%Y)*Noe zHNh-yZy?{rD_rRPO7GdQW zcikgnki<^259jjve{k;yZlCQy<3nR+(z%YyiW0egZ5m1(Zd?5=m zYdH*i_#qTHvyrRpvlrvrlj2%|k9%* zYQmfHW#m)Co%jhg(Q}}$FyqEYYZGtMB+(FMS{Vudi=2tTz0J%F(&$9ig?$#B0KEJq z6fzo020qTE2blL;q)*z=7)LAcvg^hqB-!Hjjmyl3tBf-}NP1!1hI;$lt~RT?l2!pjJdmIC=n;< zcB(MBK3jzgNnv7cn9>>@&*I%{eGNeKQz&M*#Zx7>SO{Ar+~Ixl1wgK&uV!74M1R2i zK6Fvvc%NC}2zYBzav4(`pH=3Fp~GCZvZGoyZVrRt$hEjB zkQi}uCEaKY;1ihb+Fb^T*@fI_-SS=?3u zahE)wBMXb4jUH8JsoOu=tN%e&^1+#be#U~typB1r44ni%CnC+w8SHb@^h8Iq5udi5 zNBkv;|Cvq}>S?a7H#EKgT8^&8I`pRz_qR)k>|-d^Pr!^#RhoGdXDtv7J)_00wbx$U z*M59z<#ugC%j70w%FfThDJ#WJOej-<#y1`M7AB_#a@JaoIGzfR0?8 zS55QQ$Z}Il!8B=LgL>XHGyhYj$+2~`QIwy_>%Wp$ej_J;BBy^Kr=RP~O8xVbe-RiYsDA bool: return False +def _settings_key_for_plugin(name: str) -> str: + safe = re.sub(r"[^a-z0-9]+", "_", (name or "").strip().casefold()).strip("_") + return f"update_version_{safe}" if safe else "update_version_unknown" + + +def _collect_plugin_metadata(plugin: BasisPlugin, titles: list[str]) -> dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember] | None]]: + getter = getattr(plugin, "metadata_for", None) + if not callable(getter): + return {} + collected: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember] | None]] = {} + for title in titles: + try: + labels, art, cast = getter(title) + except Exception: + continue + if isinstance(labels, dict) or isinstance(art, dict) or cast: + label_map = {str(k): str(v) for k, v in dict(labels or {}).items() if v} + art_map = {str(k): str(v) for k, v in dict(art or {}).items() if v} + collected[title] = (label_map, art_map, cast if isinstance(cast, list) else None) + return collected + + +def _needs_tmdb(labels: dict[str, str], art: dict[str, str], *, want_plot: bool, want_art: bool) -> bool: + if want_plot and not labels.get("plot"): + return True + if want_art and not (art.get("thumb") or art.get("poster") or art.get("fanart") or art.get("landscape")): + return True + return False + + +def _merge_metadata( + title: str, + tmdb_labels: dict[str, str] | None, + tmdb_art: dict[str, str] | None, + tmdb_cast: list[TmdbCastMember] | None, + plugin_meta: tuple[dict[str, str], dict[str, str], list[TmdbCastMember] | None] | None, +) -> tuple[dict[str, str], dict[str, str], list[TmdbCastMember] | None]: + labels = dict(tmdb_labels or {}) + art = dict(tmdb_art or {}) + cast = tmdb_cast + if plugin_meta is not None: + meta_labels, meta_art, meta_cast = plugin_meta + labels.update({k: str(v) for k, v in dict(meta_labels or {}).items() if v}) + art.update({k: str(v) for k, v in dict(meta_art or {}).items() if v}) + if meta_cast is not None: + cast = meta_cast + if "title" not in labels: + labels["title"] = title + return labels, art, cast + + def _sync_update_version_settings() -> None: addon = _get_addon() addon_version = "0.0.0" @@ -974,9 +1025,10 @@ def _sync_update_version_settings() -> None: "update_version_einschalten": "-", "update_version_topstreamfilm": "-", "update_version_filmpalast": "-", + "update_version_doku_streams": "-", } for plugin in _discover_plugins().values(): - key = f"update_version_{str(plugin.name).strip().lower()}" + key = _settings_key_for_plugin(str(plugin.name)) if key in versions: versions[key] = _plugin_version(plugin) for key, value in versions.items(): @@ -1072,10 +1124,24 @@ def _show_plugin_search_results(plugin_name: str, query: str) -> None: results = [str(t).strip() for t in (results or []) if t and str(t).strip()] results.sort(key=lambda value: value.casefold()) + plugin_meta = _collect_plugin_metadata(plugin, results) tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} - if results and not canceled: + show_tmdb = _tmdb_enabled() + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + prefer_source = bool(getattr(plugin, "prefer_source_metadata", False)) + tmdb_titles = list(results) + if show_tmdb and prefer_source: + tmdb_titles = [] + for title in results: + meta = plugin_meta.get(title) + meta_labels = meta[0] if meta else {} + meta_art = meta[1] if meta else {} + if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): + tmdb_titles.append(title) + if show_tmdb and tmdb_titles and not canceled: canceled = progress(35, f"{plugin_name} (1/1) Metadaten…") - tmdb_prefetched = _tmdb_labels_and_art_bulk(list(results)) + tmdb_prefetched = _tmdb_labels_and_art_bulk(list(tmdb_titles)) total_results = max(1, len(results)) for index, title in enumerate(results, start=1): @@ -1084,8 +1150,9 @@ def _show_plugin_search_results(plugin_name: str, query: str) -> None: if index == 1 or index == total_results or (index % 10 == 0): pct = 35 + int((index / float(total_results)) * 60) canceled = progress(pct, f"{plugin_name} (1/1) aufbereiten {index}/{total_results}") - info_labels, art, cast = tmdb_prefetched.get(title, _tmdb_labels_and_art(title)) - info_labels = dict(info_labels or {}) + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) info_labels.setdefault("mediatype", "tvshow") if (info_labels.get("mediatype") or "").strip().casefold() == "tvshow": info_labels.setdefault("tvshowtitle", title) @@ -1248,15 +1315,29 @@ def _show_search_results(query: str) -> None: continue results = [str(t).strip() for t in (results or []) if t and str(t).strip()] _log(f"Treffer ({plugin_name}): {len(results)}", xbmc.LOGDEBUG) + plugin_meta = _collect_plugin_metadata(plugin, results) tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} - if results: + show_tmdb = _tmdb_enabled() + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + prefer_source = bool(getattr(plugin, "prefer_source_metadata", False)) + tmdb_titles = list(results) + if show_tmdb and prefer_source: + tmdb_titles = [] + for title in results: + meta = plugin_meta.get(title) + meta_labels = meta[0] if meta else {} + meta_art = meta[1] if meta else {} + if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): + tmdb_titles.append(title) + if show_tmdb and tmdb_titles: canceled = progress( range_start + int((range_end - range_start) * 0.35), f"{plugin_name} ({plugin_index}/{total_plugins}) Metadaten…", ) if canceled: break - tmdb_prefetched = _tmdb_labels_and_art_bulk(list(results)) + tmdb_prefetched = _tmdb_labels_and_art_bulk(list(tmdb_titles)) total_results = max(1, len(results)) for title_index, title in enumerate(results, start=1): if title_index == 1 or title_index == total_results or (title_index % 10 == 0): @@ -1266,8 +1347,9 @@ def _show_search_results(query: str) -> None: ) if canceled: break - info_labels, art, cast = tmdb_prefetched.get(title, _tmdb_labels_and_art(title)) - info_labels = dict(info_labels or {}) + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) info_labels.setdefault("mediatype", "tvshow") if (info_labels.get("mediatype") or "").strip().casefold() == "tvshow": info_labels.setdefault("tvshowtitle", title) @@ -1619,6 +1701,176 @@ def _show_genres(plugin_name: str) -> None: xbmcplugin.endOfDirectory(handle) +def _show_categories(plugin_name: str) -> None: + handle = _get_handle() + _log(f"Kategorien laden: {plugin_name}") + plugin = _discover_plugins().get(plugin_name) + if plugin is None: + xbmcgui.Dialog().notification("Kategorien", "Plugin nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + getter = getattr(plugin, "categories", None) + if not callable(getter): + xbmcgui.Dialog().notification("Kategorien", "Kategorien nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + try: + categories = list(getter() or []) + except Exception as exc: + _log(f"Kategorien konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification("Kategorien", "Kategorien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + for category in categories: + category = str(category).strip() + if not category: + continue + _add_directory_item( + handle, + category, + "category_titles_page", + {"plugin": plugin_name, "category": category, "page": "1"}, + is_folder=True, + ) + xbmcplugin.endOfDirectory(handle) + + +def _show_category_titles_page(plugin_name: str, category: str, page: int = 1) -> None: + handle = _get_handle() + plugin = _discover_plugins().get(plugin_name) + if plugin is None: + xbmcgui.Dialog().notification("Kategorien", "Plugin nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + + page = max(1, int(page or 1)) + paging_getter = getattr(plugin, "titles_for_genre_page", None) + if not callable(paging_getter): + xbmcgui.Dialog().notification("Kategorien", "Paging nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000) + xbmcplugin.endOfDirectory(handle) + return + + total_pages = None + count_getter = getattr(plugin, "genre_page_count", None) + if callable(count_getter): + try: + total_pages = int(count_getter(category) or 1) + except Exception: + total_pages = None + if total_pages is not None: + page = min(page, max(1, total_pages)) + xbmcplugin.setPluginCategory(handle, f"{category} ({page}/{total_pages})") + else: + xbmcplugin.setPluginCategory(handle, f"{category} ({page})") + _set_content(handle, "movies" if (plugin_name or "").casefold() == "einschalten" else "tvshows") + + if page > 1: + _add_directory_item( + handle, + "Vorherige Seite", + "category_titles_page", + {"plugin": plugin_name, "category": category, "page": str(page - 1)}, + is_folder=True, + ) + + try: + titles = list(paging_getter(category, page) or []) + except Exception as exc: + _log(f"Kategorie-Seite konnte nicht geladen werden ({plugin_name}/{category} p{page}): {exc}", xbmc.LOGWARNING) + xbmcgui.Dialog().notification("Kategorien", "Seite konnte 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: + plugin_meta = _collect_plugin_metadata(plugin, titles) + show_tmdb = _tmdb_enabled() + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + prefer_source = bool(getattr(plugin, "prefer_source_metadata", False)) + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + tmdb_titles = list(titles) + if show_tmdb and prefer_source: + tmdb_titles = [] + for title in titles: + meta = plugin_meta.get(title) + meta_labels = meta[0] if meta else {} + meta_art = meta[1] if meta else {} + if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): + tmdb_titles.append(title) + if show_tmdb and tmdb_titles: + with _busy_dialog(): + tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) + if show_tmdb: + for title in titles: + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) + 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) + direct_play = bool( + plugin_name.casefold() == "einschalten" + and _get_setting_bool("einschalten_enable_playback", default=False) + ) + _add_directory_item( + handle, + display_label, + "play_movie" if direct_play else "seasons", + {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, + is_folder=not direct_play, + info_labels=info_labels, + art=art, + cast=cast, + ) + else: + for title in titles: + playstate = _title_playstate(plugin_name, title) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, {}, {}, None, meta) + direct_play = bool( + plugin_name.casefold() == "einschalten" + and _get_setting_bool("einschalten_enable_playback", default=False) + ) + _add_directory_item( + handle, + _label_with_playstate(title, playstate), + "play_movie" if direct_play else "seasons", + {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, + is_folder=not direct_play, + info_labels=_apply_playstate_to_info(info_labels, playstate), + art=art, + cast=cast, + ) + + show_next = False + if total_pages is not None: + show_next = page < total_pages + else: + has_more_getter = getattr(plugin, "genre_has_more", None) + if callable(has_more_getter): + try: + show_next = bool(has_more_getter(category, page)) + except Exception: + show_next = False + + if show_next: + _add_directory_item( + handle, + "Nächste Seite", + "category_titles_page", + {"plugin": plugin_name, "category": category, "page": str(page + 1)}, + is_folder=True, + ) + xbmcplugin.endOfDirectory(handle) + def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None: handle = _get_handle() plugin = _discover_plugins().get(plugin_name) @@ -1670,48 +1922,49 @@ def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None 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) - direct_play = bool( - plugin_name.casefold() == "einschalten" - and _get_setting_bool("einschalten_enable_playback", default=False) - ) - _add_directory_item( - handle, - display_label, - "play_movie" if direct_play else "seasons", - {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, - is_folder=not direct_play, - info_labels=info_labels, - art=art, - cast=cast, - ) - else: + plugin_meta = _collect_plugin_metadata(plugin, titles) + show_tmdb = show_tmdb and _tmdb_enabled() + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + prefer_source = bool(getattr(plugin, "prefer_source_metadata", False)) + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + tmdb_titles = list(titles) + if show_tmdb and prefer_source: + tmdb_titles = [] for title in titles: - playstate = _title_playstate(plugin_name, title) - direct_play = bool( - plugin_name.casefold() == "einschalten" - and _get_setting_bool("einschalten_enable_playback", default=False) - ) - _add_directory_item( - handle, - _label_with_playstate(title, playstate), - "play_movie" if direct_play else "seasons", - {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, - is_folder=not direct_play, - info_labels=_apply_playstate_to_info({"title": title}, playstate), - ) + meta = plugin_meta.get(title) + meta_labels = meta[0] if meta else {} + meta_art = meta[1] if meta else {} + if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): + tmdb_titles.append(title) + if show_tmdb and tmdb_titles: + with _busy_dialog(): + 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.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) + direct_play = bool( + plugin_name.casefold() == "einschalten" + and _get_setting_bool("einschalten_enable_playback", default=False) + ) + _add_directory_item( + handle, + display_label, + "play_movie" if direct_play else "seasons", + {"plugin": plugin_name, "title": title, **_series_url_params(plugin, title)}, + is_folder=not direct_play, + info_labels=info_labels, + art=art, + cast=cast, + ) show_next = False if total_pages is not None: @@ -2159,40 +2412,45 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None: show_tmdb = _get_setting_bool("tmdb_genre_metadata", default=False) if page_items: - if show_tmdb: - with _busy_dialog(): - tmdb_prefetched = _tmdb_labels_and_art_bulk(page_items) - for title in page_items: - 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: + plugin_meta = _collect_plugin_metadata(plugin, page_items) + show_tmdb = show_tmdb and _tmdb_enabled() + show_plot = _get_setting_bool("tmdb_show_plot", default=True) + show_art = _get_setting_bool("tmdb_show_art", default=True) + prefer_source = bool(getattr(plugin, "prefer_source_metadata", False)) + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + tmdb_titles = list(page_items) + if show_tmdb and prefer_source: + tmdb_titles = [] for title in page_items: - 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), - ) + meta = plugin_meta.get(title) + meta_labels = meta[0] if meta else {} + meta_art = meta[1] if meta else {} + if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): + tmdb_titles.append(title) + if show_tmdb and tmdb_titles: + with _busy_dialog(): + tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) + for title in page_items: + tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) + meta = plugin_meta.get(title) + info_labels, art, cast = _merge_metadata(title, tmdb_info, tmdb_art, tmdb_cast, meta) + 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, + ) if total_pages > 1 and page < total_pages: _add_directory_item( @@ -2945,6 +3203,8 @@ def run() -> None: _show_genre_sources() elif action == "genres": _show_genres(params.get("plugin", "")) + elif action == "categories": + _show_categories(params.get("plugin", "")) elif action == "new_titles": _show_new_titles( params.get("plugin", ""), @@ -2966,6 +3226,12 @@ def run() -> None: params.get("genre", ""), _parse_positive_int(params.get("page", "1"), default=1), ) + elif action == "category_titles_page": + _show_category_titles_page( + params.get("plugin", ""), + params.get("category", ""), + _parse_positive_int(params.get("page", "1"), default=1), + ) elif action == "alpha_index": _show_alpha_index(params.get("plugin", "")) elif action == "alpha_titles_page": diff --git a/addon/plugins/__pycache__/aniworld_plugin.cpython-312.pyc b/addon/plugins/__pycache__/aniworld_plugin.cpython-312.pyc deleted file mode 100644 index 44e7cd20eb9a6b290702dde5a02c06512aed50c7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 64688 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 diff --git a/addon/plugins/__pycache__/topstreamfilm_plugin.cpython-312.pyc b/addon/plugins/__pycache__/topstreamfilm_plugin.cpython-312.pyc deleted file mode 100644 index 0c62b506f299a76e65ee7b195db6704278fa0957..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 53762 zcmeFa3v?UTnI>3xkpKY_BmffN8+?fbpQ0%0{V*j`5-o|cMB1`w%QVD-_z)jT0hBBn zbd*PTs6;)c>`seHoEDw9*YwzP!hTFQbEan_%T8=>CmTSZ0tm~@x@XdRIz2n*D3QD4 zsC#?%`){GDK(Ht|on%jMpFrZRs#~}2y>;u}|NZ~>uQD?;IQ*{s;L}5oyvlL^jV`30 zK|ImiYt(YwRqk<)=XyC_!)r%0quO4rhR$^(x>0?vo}KGQ45Nl#13Nd2r1hrZzF{Q2 zH=P}gy+(F4^_p-@8_5_o_nJp7y_V6;-ptXg-mFn;uXQxLH=8}5K9VzP>$Q#Ad+i#| zL@gOc9K8-4jU&0EdA)fkW$JbEhF(|VZeedBD^=86#E!+i#WjMnse*gdhuS`9ar<+Jp9 zc&o3D&)$K-!Bs80%AqUX)?3fpdmDI1ZzG@E+r;PfuIHV-&Ah931E1g9!WZ;z)6jcL!h2SMZg56~FEkeeX^!=hO41x2oUN zh+jj??dSNK&vU%zO?(yly(u2J-d%hXU;BA|?{0oQUx(u(d^2B0nar0O5fCp z#e4Vo_Vn((V5s1H`IVet>n*@oI`EgrXY@LK1--5l9IgtUD!BZ|;_B9^dHM5pTffY0SuIAM*Kqe)nS| z69YqI?je7`?H%#sX|E7CJ27@9;2!Yt-u}Try?bI*tl2j-;2ZNbc_#wn!Vv3&`;;#r z4EatWJ40+@bSS_I3vSle?uq_EpBwc*G2}bn$!<+foEjSAePigtfuXVUz9GMFTSkMs zYa%ciaF3s*xABg+pA~$cn(+Ap{%75F?q^T=(CEJXpIaJxrQ!3es6 zPMr4%cu#lDWaFMj8d!8@#NAx)P84=Gd)z22xX+B^tM~!|_lOTyr$&537|?pu3E{`3iT4J){UcsK zT^MoU3k;3=x;$y|%$^gEb@c5$(6RSmXV?C?u5Ii>T;D#_ABgJ@VYK4LBZ==2H}zaN z>uVbs^7=j6xM@Nd@s0JP)A4ki4v!Cwq2|mz(%0$6*Xh|u9od69>KCV$#}K#i6QgJQ z1fTybM%&jn7#JOiXAk%SeSW+(2C(nc`1nZNG%`NW=M#i+A)ZdBXd#|~_Zd2UfzDBF zT6^dI&Yo_ZrFZ-Me#{iXj5m*4k0mBcA~&A3vP+Zpvuu_(o*UwQ<4AqyhXQe1VEn9~ zEtWnkDIc{lnKu{+ob_*OYC3=ZeB&ssCqJ#6coy$FH8Iea=$1cj6|dQQvDdMS8UDfX z^Hfpa(Aeqmc((EsdoO=HPpY9$Zu~T6R@^DqlyakRMU5#pUGj+eQ|{!;)%Rh|3ckKk z?{j@;y#qe~PaHH?bny)T#95!f1_X0M7(Y8Q?&agUbK}0rZ1&>)BjXc%WB zk7p*^4v$X`jf{AkS{v89YYvX{L)#O}+T(6Jiy3^}ck19!psBTGb7RXUlpqLDk0p7= z=icw@KQr!`+#T>e7igjZtM}rGp?)@3o1UYby5~}JM@P1QYUldKt@X7{wd`!mq?7kG zw0G3=zQi5hSmH*;8K3WLgLh=;oX?}5%y``I6B^p^>VbG#TYtarY+%wV9vco}NljoR z;u(9#vA+8W;LzMUEsRfMa^p?qUjnpi?rlWYRnEutYJjq}K-`*M9j^r<(J@3tXL@!9 zbj|Y9=;}hc#~3#OPyj+57z#|55x^G4u*MUR$xYA_aB0K`AouvOLyd7ACS=^epjli; z3xJE80g(EIA+{iC?Hc>~#=N7xzP@-yUmu{�Z^R`uaXK;T=ibN$>09$NQ0O?dwZ| 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 GQ^0>!qBrsY diff --git a/addon/plugins/dokustreams_plugin.py b/addon/plugins/dokustreams_plugin.py new file mode 100644 index 0000000..047a652 --- /dev/null +++ b/addon/plugins/dokustreams_plugin.py @@ -0,0 +1,476 @@ +"""Doku-Streams (doku-streams.com) Integration.""" + +from __future__ import annotations + +from dataclasses import dataclass +import re +from urllib.parse import quote +from typing import TYPE_CHECKING, Any, Dict, List, Optional, TypeAlias + +try: # pragma: no cover - optional dependency + import requests + from bs4 import BeautifulSoup # type: ignore[import-not-found] +except ImportError as exc: # pragma: no cover - optional dependency + requests = None + BeautifulSoup = None + REQUESTS_AVAILABLE = False + REQUESTS_IMPORT_ERROR = exc +else: + REQUESTS_AVAILABLE = True + REQUESTS_IMPORT_ERROR = None + +from plugin_interface import BasisPlugin +from plugin_helpers import dump_response_html, get_setting_bool, get_setting_string, log_error, log_url, notify_url +from http_session_pool import get_requests_session + +if TYPE_CHECKING: # pragma: no cover + from requests import Session as RequestsSession + from bs4 import BeautifulSoup as BeautifulSoupT # type: ignore[import-not-found] +else: # pragma: no cover + RequestsSession: TypeAlias = Any + BeautifulSoupT: TypeAlias = Any + + +ADDON_ID = "plugin.video.viewit" +SETTING_BASE_URL = "doku_streams_base_url" +DEFAULT_BASE_URL = "https://doku-streams.com" +MOST_VIEWED_PATH = "/meistgesehene/" +DEFAULT_TIMEOUT = 20 +GLOBAL_SETTING_LOG_URLS = "debug_log_urls" +GLOBAL_SETTING_DUMP_HTML = "debug_dump_html" +GLOBAL_SETTING_SHOW_URL_INFO = "debug_show_url_info" +GLOBAL_SETTING_LOG_ERRORS = "debug_log_errors" +SETTING_LOG_URLS = "log_urls_dokustreams" +SETTING_DUMP_HTML = "dump_html_dokustreams" +SETTING_SHOW_URL_INFO = "show_url_info_dokustreams" +SETTING_LOG_ERRORS = "log_errors_dokustreams" +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", + "Accept-Language": "de-DE,de;q=0.9,en;q=0.8", + "Connection": "keep-alive", +} + + +@dataclass(frozen=True) +class SearchHit: + title: str + url: str + plot: str = "" + poster: str = "" + + +def _extract_last_page(soup: BeautifulSoupT) -> int: + max_page = 1 + if not soup: + return max_page + for anchor in soup.select("nav.navigation a[href], nav.pagination a[href], a.page-numbers[href]"): + text = (anchor.get_text(" ", strip=True) or "").strip() + for candidate in (text, (anchor.get("href") or "").strip()): + for value in re.findall(r"/page/(\\d+)/", candidate): + try: + max_page = max(max_page, int(value)) + except Exception: + continue + for value in re.findall(r"(\\d+)", candidate): + try: + max_page = max(max_page, int(value)) + except Exception: + continue + return max_page + + +def _extract_summary_and_poster(article: BeautifulSoupT) -> tuple[str, str]: + summary = "" + if article: + summary_box = article.select_one("div.entry-summary") + if summary_box is not None: + for p in summary_box.find_all("p"): + text = (p.get_text(" ", strip=True) or "").strip() + if text: + summary = text + break + poster = "" + if article: + img = article.select_one("div.entry-thumb img") + if img is not None: + poster = (img.get("data-src") or "").strip() or (img.get("src") or "").strip() + if "lazy_placeholder" in poster and img.get("data-src"): + poster = (img.get("data-src") or "").strip() + poster = _absolute_url(poster) + return summary, poster + + +def _parse_listing_hits(soup: BeautifulSoupT, *, query: str = "") -> List[SearchHit]: + hits: List[SearchHit] = [] + if not soup: + return hits + seen_titles: set[str] = set() + seen_urls: set[str] = set() + for article in soup.select("article[id^='post-']"): + anchor = article.select_one("h2.entry-title a[href]") + if anchor is None: + continue + href = (anchor.get("href") or "").strip() + title = (anchor.get_text(" ", strip=True) or "").strip() + if not href or not title: + continue + if query and not _matches_query(query, title=title): + continue + url = _absolute_url(href).split("#", 1)[0].split("?", 1)[0].rstrip("/") + title_key = title.casefold() + url_key = url.casefold() + if title_key in seen_titles or url_key in seen_urls: + continue + seen_titles.add(title_key) + seen_urls.add(url_key) + _log_url_event(url, kind="PARSE") + summary, poster = _extract_summary_and_poster(article) + hits.append(SearchHit(title=title, url=url, plot=summary, poster=poster)) + return hits + + +def _get_base_url() -> str: + base = get_setting_string(ADDON_ID, SETTING_BASE_URL, default=DEFAULT_BASE_URL).strip() + if not base: + base = DEFAULT_BASE_URL + return base.rstrip("/") + + +def _absolute_url(url: str) -> str: + url = (url or "").strip() + if not url: + return "" + if url.startswith("http://") or url.startswith("https://"): + return url + if url.startswith("//"): + return f"https:{url}" + if url.startswith("/"): + return f"{_get_base_url()}{url}" + return f"{_get_base_url()}/{url.lstrip('/')}" + + +def _normalize_search_text(value: str) -> str: + value = (value or "").casefold() + value = re.sub(r"[^a-z0-9]+", " ", value) + value = re.sub(r"\s+", " ", value).strip() + return value + + +def _matches_query(query: str, *, title: str) -> bool: + normalized_query = _normalize_search_text(query) + if not normalized_query: + return False + haystack = f" {_normalize_search_text(title)} " + return f" {normalized_query} " in haystack + + +def _log_url_event(url: str, *, kind: str = "VISIT") -> None: + log_url( + ADDON_ID, + enabled_setting_id=GLOBAL_SETTING_LOG_URLS, + plugin_setting_id=SETTING_LOG_URLS, + log_filename="dokustreams_urls.log", + url=url, + kind=kind, + ) + + +def _log_visit(url: str) -> None: + _log_url_event(url, kind="VISIT") + notify_url( + ADDON_ID, + heading="Doku-Streams", + url=url, + enabled_setting_id=GLOBAL_SETTING_SHOW_URL_INFO, + plugin_setting_id=SETTING_SHOW_URL_INFO, + ) + + +def _log_response_html(url: str, body: str) -> None: + dump_response_html( + ADDON_ID, + enabled_setting_id=GLOBAL_SETTING_DUMP_HTML, + plugin_setting_id=SETTING_DUMP_HTML, + url=url, + body=body, + filename_prefix="dokustreams_response", + ) + + +def _log_error_message(message: str) -> None: + log_error( + ADDON_ID, + enabled_setting_id=GLOBAL_SETTING_LOG_ERRORS, + plugin_setting_id=SETTING_LOG_ERRORS, + log_filename="dokustreams_errors.log", + message=message, + ) + + +def _get_soup(url: str, *, session: Optional[RequestsSession] = None) -> BeautifulSoupT: + if requests is None or BeautifulSoup is None: + raise RuntimeError("requests/bs4 sind nicht verfuegbar.") + _log_visit(url) + sess = session or get_requests_session("dokustreams", headers=HEADERS) + try: + response = sess.get(url, headers=HEADERS, timeout=DEFAULT_TIMEOUT) + response.raise_for_status() + except Exception as exc: + _log_error_message(f"GET {url} failed: {exc}") + raise + if response.url and response.url != url: + _log_url_event(response.url, kind="REDIRECT") + _log_response_html(url, response.text) + return BeautifulSoup(response.text, "html.parser") + + +class DokuStreamsPlugin(BasisPlugin): + name = "Doku-Streams" + version = "1.0.0" + prefer_source_metadata = True + + def __init__(self) -> None: + self._title_to_url: Dict[str, str] = {} + self._category_to_url: Dict[str, str] = {} + self._category_page_count_cache: Dict[str, int] = {} + self._popular_cache: Optional[List[SearchHit]] = None + self._title_meta: Dict[str, tuple[str, str]] = {} + self._requests_available = REQUESTS_AVAILABLE + self.is_available = True + self.unavailable_reason: Optional[str] = None + if not self._requests_available: # pragma: no cover - optional dependency + self.is_available = False + self.unavailable_reason = ( + "requests/bs4 fehlen. Installiere 'requests' und 'beautifulsoup4'." + ) + if REQUESTS_IMPORT_ERROR: + print(f"DokuStreamsPlugin Importfehler: {REQUESTS_IMPORT_ERROR}") + + async def search_titles(self, query: str) -> List[str]: + hits = self._search_hits(query) + self._title_to_url = {hit.title: hit.url for hit in hits if hit.title and hit.url} + for hit in hits: + if hit.title: + self._title_meta[hit.title] = (hit.plot, hit.poster) + titles = [hit.title for hit in hits if hit.title] + titles.sort(key=lambda value: value.casefold()) + return titles + + def _search_hits(self, query: str) -> List[SearchHit]: + query = (query or "").strip() + if not query or not self._requests_available: + return [] + search_url = _absolute_url(f"/?s={quote(query)}") + session = get_requests_session("dokustreams", headers=HEADERS) + try: + soup = _get_soup(search_url, session=session) + except Exception: + return [] + return _parse_listing_hits(soup, query=query) + + def capabilities(self) -> set[str]: + return {"genres", "popular_series"} + + def _categories_url(self) -> str: + return _absolute_url("/kategorien/") + + def _parse_categories(self, soup: BeautifulSoupT) -> Dict[str, str]: + categories: Dict[str, str] = {} + if not soup: + return categories + root = soup.select_one("ul.nested-category-list") + if root is None: + return categories + + def clean_name(value: str) -> str: + value = (value or "").strip() + return re.sub(r"\\s*\\(\\d+\\)\\s*$", "", value).strip() + + def walk(ul, parents: List[str]) -> None: + for li in ul.find_all("li", recursive=False): + anchor = li.find("a", href=True) + if anchor is None: + continue + name = clean_name(anchor.get_text(" ", strip=True) or "") + href = (anchor.get("href") or "").strip() + if not name or not href: + continue + child_ul = li.find("ul", class_="nested-category-list") + if child_ul is not None: + walk(child_ul, parents + [name]) + else: + if parents: + label = " \u2192 ".join(parents + [name]) + categories[label] = _absolute_url(href) + + walk(root, []) + return categories + + def _parse_top_categories(self, soup: BeautifulSoupT) -> Dict[str, str]: + categories: Dict[str, str] = {} + if not soup: + return categories + root = soup.select_one("ul.nested-category-list") + if root is None: + return categories + for li in root.find_all("li", recursive=False): + anchor = li.find("a", href=True) + if anchor is None: + continue + name = (anchor.get_text(" ", strip=True) or "").strip() + href = (anchor.get("href") or "").strip() + if not name or not href: + continue + categories[name] = _absolute_url(href) + return categories + + def genres(self) -> List[str]: + if not self._requests_available: + return [] + if self._category_to_url: + return sorted(self._category_to_url.keys(), key=lambda value: value.casefold()) + try: + soup = _get_soup(self._categories_url(), session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return [] + parsed = self._parse_categories(soup) + if parsed: + self._category_to_url = dict(parsed) + return sorted(self._category_to_url.keys(), key=lambda value: value.casefold()) + + def categories(self) -> List[str]: + if not self._requests_available: + return [] + try: + soup = _get_soup(self._categories_url(), session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return [] + parsed = self._parse_top_categories(soup) + if parsed: + for key, value in parsed.items(): + self._category_to_url.setdefault(key, value) + return list(parsed.keys()) + + def genre_page_count(self, genre: str) -> int: + genre = (genre or "").strip() + if not genre: + return 1 + if genre in self._category_page_count_cache: + return max(1, int(self._category_page_count_cache.get(genre, 1))) + if not self._category_to_url: + self.genres() + base_url = self._category_to_url.get(genre, "") + if not base_url: + return 1 + try: + soup = _get_soup(base_url, session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return 1 + pages = _extract_last_page(soup) + self._category_page_count_cache[genre] = max(1, pages) + return self._category_page_count_cache[genre] + + def titles_for_genre_page(self, genre: str, page: int) -> List[str]: + genre = (genre or "").strip() + if not genre or not self._requests_available: + return [] + if not self._category_to_url: + self.genres() + base_url = self._category_to_url.get(genre, "") + if not base_url: + return [] + page = max(1, int(page or 1)) + url = base_url if page == 1 else f"{base_url.rstrip('/')}/page/{page}/" + try: + soup = _get_soup(url, session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return [] + hits = _parse_listing_hits(soup) + for hit in hits: + if hit.title: + self._title_meta[hit.title] = (hit.plot, hit.poster) + titles = [hit.title for hit in hits if hit.title] + self._title_to_url.update({hit.title: hit.url for hit in hits if hit.title and hit.url}) + return titles + + def titles_for_genre(self, genre: str) -> List[str]: + titles = self.titles_for_genre_page(genre, 1) + titles.sort(key=lambda value: value.casefold()) + return titles + + def _most_viewed_url(self) -> str: + return _absolute_url(MOST_VIEWED_PATH) + + def popular_series(self) -> List[str]: + if not self._requests_available: + return [] + if self._popular_cache is not None: + titles = [hit.title for hit in self._popular_cache if hit.title] + titles.sort(key=lambda value: value.casefold()) + return titles + try: + soup = _get_soup(self._most_viewed_url(), session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return [] + hits = _parse_listing_hits(soup) + self._popular_cache = list(hits) + self._title_to_url.update({hit.title: hit.url for hit in hits if hit.title and hit.url}) + for hit in hits: + if hit.title: + self._title_meta[hit.title] = (hit.plot, hit.poster) + titles = [hit.title for hit in hits if hit.title] + titles.sort(key=lambda value: value.casefold()) + return titles + + def metadata_for(self, title: str) -> tuple[dict[str, str], dict[str, str], list[object] | None]: + title = (title or "").strip() + if not title: + return {}, {}, None + plot, poster = self._title_meta.get(title, ("", "")) + info: dict[str, str] = {"title": title} + if plot: + info["plot"] = plot + art: dict[str, str] = {} + if poster: + art = {"thumb": poster, "poster": poster} + return info, art, None + + def seasons_for(self, title: str) -> List[str]: + title = (title or "").strip() + if not title or title not in self._title_to_url: + return [] + return ["Stream"] + + def episodes_for(self, title: str, season: str) -> List[str]: + title = (title or "").strip() + if not title or title not in self._title_to_url: + return [] + return [title] + + def stream_link_for(self, title: str, season: str, episode: str) -> Optional[str]: + title = (title or "").strip() + if not title: + return None + url = self._title_to_url.get(title) + if not url: + return None + if not self._requests_available: + return None + try: + soup = _get_soup(url, session=get_requests_session("dokustreams", headers=HEADERS)) + except Exception: + return None + iframe = soup.select_one("div.fluid-width-video-wrapper iframe[src]") + if iframe is None: + iframe = soup.select_one("iframe[src*='youtube'], iframe[src*='vimeo'], iframe[src]") + if iframe is None: + return None + src = (iframe.get("src") or "").strip() + if not src: + return None + return _absolute_url(src) + + +# Alias für die automatische Plugin-Erkennung. +Plugin = DokuStreamsPlugin diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index 90fa06b..d9f7c76 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -45,6 +45,9 @@ + + + @@ -71,5 +74,6 @@ +