diff --git a/addon/default.py b/addon/default.py index 63b4e4e..f1c873f 100644 --- a/addon/default.py +++ b/addon/default.py @@ -147,6 +147,45 @@ def _busy_dialog(): _busy_close() +@contextmanager +def _progress_dialog(heading: str, message: str = ""): + """Zeigt einen Fortschrittsdialog in Kodi und liefert eine Update-Funktion.""" + dialog = None + try: # pragma: no cover - Kodi runtime + if xbmcgui is not None and hasattr(xbmcgui, "DialogProgress"): + dialog = xbmcgui.DialogProgress() + dialog.create(heading, message) + except Exception: + dialog = None + + def _update(percent: int, text: str = "") -> bool: + if dialog is None: + return False + percent = max(0, min(100, int(percent))) + try: # Kodi Matrix/Nexus + dialog.update(percent, text) + except TypeError: + try: # Kodi Leia fallback + dialog.update(percent, text, "", "") + except Exception: + pass + except Exception: + pass + try: + return bool(dialog.iscanceled()) + except Exception: + return False + + try: + yield _update + finally: + if dialog is not None: + try: + dialog.close() + except Exception: + pass + + def _get_handle() -> int: return int(sys.argv[1]) if len(sys.argv) > 1 else -1 @@ -919,41 +958,69 @@ def _show_plugin_search_results(plugin_name: str, query: str) -> None: _set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows") _log(f"Suche nach Titeln (Plugin={plugin_name}): {query}") + list_items: list[dict[str, object]] = [] + canceled = False try: - results = _run_async(plugin.search_titles(query)) + with _progress_dialog("Suche läuft", f"{plugin_name} (1/1) starte…") as progress: + canceled = progress(5, f"{plugin_name} (1/1) Suche…") + results = _run_async(plugin.search_titles(query)) + results = [str(t).strip() for t in (results or []) if t and str(t).strip()] + results.sort(key=lambda value: value.casefold()) + + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + if results and not canceled: + canceled = progress(35, f"{plugin_name} (1/1) Metadaten…") + tmdb_prefetched = _tmdb_labels_and_art_bulk(list(results)) + + total_results = max(1, len(results)) + for index, title in enumerate(results, start=1): + if canceled: + break + 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 {}) + 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) + merged_info = _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)) + extra_params = _series_url_params(plugin, title) + list_items.append( + { + "label": display_label, + "action": "play_movie" if direct_play else "seasons", + "params": {"plugin": plugin_name, "title": title, **extra_params}, + "is_folder": (not direct_play), + "info_labels": merged_info, + "art": art, + "cast": cast, + } + ) except Exception as exc: _log(f"Suche fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING) xbmcgui.Dialog().notification("Suche", "Suche fehlgeschlagen.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcplugin.endOfDirectory(handle) return + if canceled and not list_items: + xbmcgui.Dialog().notification("Suche", "Suche abgebrochen.", xbmcgui.NOTIFICATION_INFO, 2500) + xbmcplugin.endOfDirectory(handle) + return - results = [str(t).strip() for t in (results or []) if t and str(t).strip()] - results.sort(key=lambda value: value.casefold()) - tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} - if results: - with _busy_dialog(): - tmdb_prefetched = _tmdb_labels_and_art_bulk(list(results)) - for title in results: - 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) - merged_info = _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)) - extra_params = _series_url_params(plugin, title) + for item in list_items: _add_directory_item( handle, - display_label, - "play_movie" if direct_play else "seasons", - {"plugin": plugin_name, "title": title, **extra_params}, - is_folder=not direct_play, - info_labels=merged_info, - art=art, - cast=cast, + str(item["label"]), + str(item["action"]), + dict(item["params"]), + is_folder=bool(item["is_folder"]), + info_labels=item["info_labels"], + art=item["art"], + cast=item["cast"], ) xbmcplugin.endOfDirectory(handle) @@ -1058,42 +1125,87 @@ def _show_search_results(query: str) -> None: xbmcgui.Dialog().notification("Suche", "Keine Plugins gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcplugin.endOfDirectory(handle) return - for plugin_name, plugin in plugins.items(): - try: - results = _run_async(plugin.search_titles(query)) - except Exception as exc: - _log(f"Suche fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING) - continue - _log(f"Treffer ({plugin_name}): {len(results)}", xbmc.LOGDEBUG) - tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} - if results: - with _busy_dialog(): + list_items: list[dict[str, object]] = [] + canceled = False + plugin_entries = list(plugins.items()) + total_plugins = max(1, len(plugin_entries)) + with _progress_dialog("Suche läuft", "Suche gestartet…") as progress: + for plugin_index, (plugin_name, plugin) in enumerate(plugin_entries, start=1): + range_start = int(((plugin_index - 1) / float(total_plugins)) * 100) + range_end = int((plugin_index / float(total_plugins)) * 100) + canceled = progress(range_start, f"{plugin_name} ({plugin_index}/{total_plugins}) Suche…") + if canceled: + break + try: + results = _run_async(plugin.search_titles(query)) + except Exception as exc: + _log(f"Suche fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING) + 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) + tmdb_prefetched: dict[str, tuple[dict[str, str], dict[str, str], list[TmdbCastMember]]] = {} + if results: + 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)) - for title in results: - 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) - merged_info = _apply_playstate_to_info(dict(info_labels), playstate) - label = _label_with_duration(title, info_labels) - label = _label_with_playstate(label, playstate) - label = f"{label} [{plugin_name}]" - direct_play = bool( - plugin_name.casefold() == "einschalten" and _get_setting_bool("einschalten_enable_playback", default=False) - ) - extra_params = _series_url_params(plugin, title) - _add_directory_item( - handle, - label, - "play_movie" if direct_play else "seasons", - {"plugin": plugin_name, "title": title, **extra_params}, - is_folder=not direct_play, - info_labels=merged_info, - art=art, - cast=cast, - ) + 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): + canceled = progress( + range_start + int((range_end - range_start) * (0.35 + 0.65 * (title_index / float(total_results)))), + f"{plugin_name} ({plugin_index}/{total_plugins}) aufbereiten {title_index}/{total_results}", + ) + if canceled: + break + 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) + merged_info = _apply_playstate_to_info(dict(info_labels), playstate) + label = _label_with_duration(title, info_labels) + label = _label_with_playstate(label, playstate) + label = f"{label} [{plugin_name}]" + direct_play = bool( + plugin_name.casefold() == "einschalten" and _get_setting_bool("einschalten_enable_playback", default=False) + ) + extra_params = _series_url_params(plugin, title) + list_items.append( + { + "label": label, + "action": "play_movie" if direct_play else "seasons", + "params": {"plugin": plugin_name, "title": title, **extra_params}, + "is_folder": (not direct_play), + "info_labels": merged_info, + "art": art, + "cast": cast, + } + ) + if canceled: + break + if not canceled: + progress(100, "Suche abgeschlossen") + if canceled and not list_items: + xbmcgui.Dialog().notification("Suche", "Suche abgebrochen.", xbmcgui.NOTIFICATION_INFO, 2500) + xbmcplugin.endOfDirectory(handle) + return + + for item in list_items: + _add_directory_item( + handle, + str(item["label"]), + str(item["action"]), + dict(item["params"]), + is_folder=bool(item["is_folder"]), + info_labels=item["info_labels"], + art=item["art"], + cast=item["cast"], + ) xbmcplugin.endOfDirectory(handle)