Unify menu labels, centralize hoster URL normalization, and add auto-update toggle
This commit is contained in:
439
addon/default.py
439
addon/default.py
@@ -18,6 +18,7 @@ import os
|
||||
import re
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
@@ -104,6 +105,7 @@ except ImportError: # pragma: no cover - allow importing outside Kodi (e.g. lin
|
||||
|
||||
from plugin_interface import BasisPlugin
|
||||
from http_session_pool import close_all_sessions
|
||||
from plugin_helpers import normalize_resolved_stream_url
|
||||
from metadata_utils import (
|
||||
collect_plugin_metadata as _collect_plugin_metadata,
|
||||
merge_metadata as _merge_metadata,
|
||||
@@ -127,6 +129,8 @@ _PLAYSTATE_CACHE: dict[str, dict[str, object]] | None = None
|
||||
_PLAYSTATE_LOCK = threading.RLock()
|
||||
_TMDB_LOCK = threading.RLock()
|
||||
WATCHED_THRESHOLD = 0.9
|
||||
POPULAR_MENU_LABEL = "Haeufig gesehen"
|
||||
LATEST_MENU_LABEL = "Neuste Titel"
|
||||
|
||||
atexit.register(close_all_sessions)
|
||||
|
||||
@@ -231,6 +235,15 @@ def _progress_dialog(heading: str, message: str = ""):
|
||||
pass
|
||||
|
||||
|
||||
def _run_with_progress(heading: str, message: str, loader):
|
||||
"""Fuehrt eine Ladefunktion mit sichtbarem Fortschrittsdialog aus."""
|
||||
with _progress_dialog(heading, message) as progress:
|
||||
progress(10, message)
|
||||
result = loader()
|
||||
progress(100, "Fertig")
|
||||
return result
|
||||
|
||||
|
||||
def _method_accepts_kwarg(method: object, kwarg_name: str) -> bool:
|
||||
if not callable(method):
|
||||
return False
|
||||
@@ -297,117 +310,26 @@ def _playstate_path() -> str:
|
||||
|
||||
|
||||
def _load_playstate() -> dict[str, dict[str, object]]:
|
||||
global _PLAYSTATE_CACHE
|
||||
with _PLAYSTATE_LOCK:
|
||||
if _PLAYSTATE_CACHE is not None:
|
||||
return _PLAYSTATE_CACHE
|
||||
path = _playstate_path()
|
||||
try:
|
||||
if xbmcvfs and xbmcvfs.exists(path):
|
||||
handle = xbmcvfs.File(path)
|
||||
raw = handle.read()
|
||||
handle.close()
|
||||
else:
|
||||
with open(path, "r", encoding="utf-8") as handle:
|
||||
raw = handle.read()
|
||||
data = json.loads(raw or "{}")
|
||||
if isinstance(data, dict):
|
||||
normalized: dict[str, dict[str, object]] = {}
|
||||
for key, value in data.items():
|
||||
if isinstance(key, str) and isinstance(value, dict):
|
||||
normalized[key] = dict(value)
|
||||
_PLAYSTATE_CACHE = normalized
|
||||
return normalized
|
||||
except Exception:
|
||||
pass
|
||||
_PLAYSTATE_CACHE = {}
|
||||
return {}
|
||||
return {}
|
||||
|
||||
|
||||
def _save_playstate(state: dict[str, dict[str, object]]) -> None:
|
||||
global _PLAYSTATE_CACHE
|
||||
with _PLAYSTATE_LOCK:
|
||||
_PLAYSTATE_CACHE = state
|
||||
path = _playstate_path()
|
||||
try:
|
||||
payload = json.dumps(state, ensure_ascii=False, sort_keys=True)
|
||||
except Exception:
|
||||
return
|
||||
try:
|
||||
if xbmcvfs:
|
||||
directory = os.path.dirname(path)
|
||||
if directory and not xbmcvfs.exists(directory):
|
||||
xbmcvfs.mkdirs(directory)
|
||||
handle = xbmcvfs.File(path, "w")
|
||||
handle.write(payload)
|
||||
handle.close()
|
||||
else:
|
||||
with open(path, "w", encoding="utf-8") as handle:
|
||||
handle.write(payload)
|
||||
except Exception:
|
||||
return
|
||||
return
|
||||
|
||||
|
||||
def _get_playstate(key: str) -> dict[str, object]:
|
||||
return dict(_load_playstate().get(key, {}) or {})
|
||||
return {}
|
||||
|
||||
|
||||
def _set_playstate(key: str, value: dict[str, object]) -> None:
|
||||
state = _load_playstate()
|
||||
if value:
|
||||
state[key] = dict(value)
|
||||
else:
|
||||
state.pop(key, None)
|
||||
_save_playstate(state)
|
||||
return
|
||||
|
||||
|
||||
def _apply_playstate_to_info(info_labels: dict[str, object], playstate: dict[str, object]) -> dict[str, object]:
|
||||
info_labels = dict(info_labels or {})
|
||||
watched = bool(playstate.get("watched") or False)
|
||||
resume_position = playstate.get("resume_position")
|
||||
resume_total = playstate.get("resume_total")
|
||||
if watched:
|
||||
info_labels["playcount"] = 1
|
||||
info_labels.pop("resume_position", None)
|
||||
info_labels.pop("resume_total", None)
|
||||
else:
|
||||
try:
|
||||
pos = int(resume_position) if resume_position is not None else 0
|
||||
tot = int(resume_total) if resume_total is not None else 0
|
||||
except Exception:
|
||||
pos, tot = 0, 0
|
||||
if pos > 0 and tot > 0:
|
||||
info_labels["resume_position"] = pos
|
||||
info_labels["resume_total"] = tot
|
||||
return info_labels
|
||||
|
||||
|
||||
def _time_label(seconds: int) -> str:
|
||||
try:
|
||||
seconds = int(seconds or 0)
|
||||
except Exception:
|
||||
seconds = 0
|
||||
if seconds <= 0:
|
||||
return ""
|
||||
hours = seconds // 3600
|
||||
minutes = (seconds % 3600) // 60
|
||||
secs = seconds % 60
|
||||
if hours > 0:
|
||||
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
|
||||
return f"{minutes:02d}:{secs:02d}"
|
||||
return dict(info_labels or {})
|
||||
|
||||
|
||||
def _label_with_playstate(label: str, playstate: dict[str, object]) -> str:
|
||||
watched = bool(playstate.get("watched") or False)
|
||||
if watched:
|
||||
return f"✓ {label}"
|
||||
resume_pos = playstate.get("resume_position")
|
||||
try:
|
||||
pos = int(resume_pos) if resume_pos is not None else 0
|
||||
except Exception:
|
||||
pos = 0
|
||||
if pos > 0:
|
||||
return f"↩ {_time_label(pos)} {label}"
|
||||
return label
|
||||
|
||||
|
||||
@@ -1047,6 +969,33 @@ def _normalize_update_info_url(raw: str) -> str:
|
||||
return value.rstrip("/") + "/addons.xml"
|
||||
|
||||
|
||||
UPDATE_CHANNEL_MAIN = 0
|
||||
UPDATE_CHANNEL_NIGHTLY = 1
|
||||
UPDATE_CHANNEL_CUSTOM = 2
|
||||
AUTO_UPDATE_INTERVAL_SEC = 6 * 60 * 60
|
||||
|
||||
|
||||
def _selected_update_channel() -> int:
|
||||
channel = _get_setting_int("update_channel", default=UPDATE_CHANNEL_MAIN)
|
||||
if channel not in {UPDATE_CHANNEL_MAIN, UPDATE_CHANNEL_NIGHTLY, UPDATE_CHANNEL_CUSTOM}:
|
||||
return UPDATE_CHANNEL_MAIN
|
||||
return channel
|
||||
|
||||
|
||||
def _resolve_update_info_url() -> str:
|
||||
channel = _selected_update_channel()
|
||||
if channel == UPDATE_CHANNEL_NIGHTLY:
|
||||
raw = _get_setting_string("update_repo_url_nightly")
|
||||
elif channel == UPDATE_CHANNEL_CUSTOM:
|
||||
raw = _get_setting_string("update_repo_url")
|
||||
else:
|
||||
raw = _get_setting_string("update_repo_url_main")
|
||||
info_url = _normalize_update_info_url(raw)
|
||||
# Legacy-Setting beibehalten, damit bestehende Installationen und alte Builds weiterlaufen.
|
||||
_set_setting_string("update_repo_url", info_url)
|
||||
return info_url
|
||||
|
||||
|
||||
def _repo_addon_xml_path() -> str:
|
||||
if xbmcvfs is None:
|
||||
return ""
|
||||
@@ -1141,11 +1090,8 @@ def _show_plugin_menu(plugin_name: str) -> None:
|
||||
|
||||
_add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True)
|
||||
|
||||
if _plugin_has_capability(plugin, "new_titles"):
|
||||
_add_directory_item(handle, "Neue Titel", "new_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True)
|
||||
|
||||
if _plugin_has_capability(plugin, "latest_episodes"):
|
||||
_add_directory_item(handle, "Neueste Folgen", "latest_episodes", {"plugin": plugin_name, "page": "1"}, is_folder=True)
|
||||
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)
|
||||
|
||||
if _plugin_has_capability(plugin, "genres"):
|
||||
_add_directory_item(handle, "Genres", "genres", {"plugin": plugin_name}, is_folder=True)
|
||||
@@ -1157,7 +1103,7 @@ def _show_plugin_menu(plugin_name: str) -> None:
|
||||
_add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True)
|
||||
|
||||
if _plugin_has_capability(plugin, "popular_series"):
|
||||
_add_directory_item(handle, "Beliebte Serien", "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True)
|
||||
_add_directory_item(handle, POPULAR_MENU_LABEL, "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True)
|
||||
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
|
||||
@@ -1921,7 +1867,11 @@ def _show_genres(plugin_name: str) -> None:
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
try:
|
||||
genres = plugin.genres()
|
||||
genres = _run_with_progress(
|
||||
"Genres",
|
||||
f"{plugin_name}: Genres werden geladen...",
|
||||
lambda: plugin.genres(),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Genres konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Genres", "Genres konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -1963,7 +1913,11 @@ def _show_categories(plugin_name: str) -> None:
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
try:
|
||||
categories = list(getter() or [])
|
||||
categories = _run_with_progress(
|
||||
"Kategorien",
|
||||
f"{plugin_name}: Kategorien werden geladen...",
|
||||
lambda: 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)
|
||||
@@ -2022,7 +1976,11 @@ def _show_category_titles_page(plugin_name: str, category: str, page: int = 1) -
|
||||
)
|
||||
|
||||
try:
|
||||
titles = list(paging_getter(category, page) or [])
|
||||
titles = _run_with_progress(
|
||||
"Kategorien",
|
||||
f"{plugin_name}: {category} Seite {page} wird geladen...",
|
||||
lambda: 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)
|
||||
@@ -2092,7 +2050,7 @@ def _show_category_titles_page(plugin_name: str, category: str, page: int = 1) -
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"category_titles_page",
|
||||
{"plugin": plugin_name, "category": category, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -2138,7 +2096,11 @@ def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None
|
||||
)
|
||||
|
||||
try:
|
||||
titles = list(paging_getter(genre, page) or [])
|
||||
titles = _run_with_progress(
|
||||
"Genres",
|
||||
f"{plugin_name}: {genre} Seite {page} wird geladen...",
|
||||
lambda: list(paging_getter(genre, page) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Genre-Seite konnte nicht geladen werden ({plugin_name}/{genre} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Genres", "Seite konnte nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -2208,7 +2170,7 @@ def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"genre_titles_page",
|
||||
{"plugin": plugin_name, "genre": genre, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -2230,7 +2192,11 @@ def _show_alpha_index(plugin_name: str) -> None:
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
try:
|
||||
letters = list(getter() or [])
|
||||
letters = _run_with_progress(
|
||||
"A-Z",
|
||||
f"{plugin_name}: Index wird geladen...",
|
||||
lambda: list(getter() or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"A-Z konnte nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("A-Z", "A-Z konnte nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -2289,7 +2255,11 @@ def _show_alpha_titles_page(plugin_name: str, letter: str, page: int = 1) -> Non
|
||||
)
|
||||
|
||||
try:
|
||||
titles = list(paging_getter(letter, page) or [])
|
||||
titles = _run_with_progress(
|
||||
"A-Z",
|
||||
f"{plugin_name}: {letter} Seite {page} wird geladen...",
|
||||
lambda: list(paging_getter(letter, page) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"A-Z Seite konnte nicht geladen werden ({plugin_name}/{letter} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("A-Z", "Seite konnte nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -2353,7 +2323,7 @@ def _show_alpha_titles_page(plugin_name: str, letter: str, page: int = 1) -> Non
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"alpha_titles_page",
|
||||
{"plugin": plugin_name, "letter": letter, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -2401,7 +2371,11 @@ def _show_series_catalog(plugin_name: str, page: int = 1) -> None:
|
||||
)
|
||||
|
||||
try:
|
||||
titles = list(paging_getter(page) or [])
|
||||
titles = _run_with_progress(
|
||||
"Serien",
|
||||
f"{plugin_name}: Seite {page} wird geladen...",
|
||||
lambda: list(paging_getter(page) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Serien konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Serien", "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -2468,7 +2442,7 @@ def _show_series_catalog(plugin_name: str, page: int = 1) -> None:
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"series_catalog",
|
||||
{"plugin": plugin_name, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -2606,22 +2580,30 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
if plugin_name:
|
||||
plugin = _discover_plugins().get(plugin_name)
|
||||
if plugin is None or not _plugin_has_capability(plugin, "popular_series"):
|
||||
xbmcgui.Dialog().notification("Beliebte Serien", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
try:
|
||||
popular_getter = getattr(plugin, "popular_series", None)
|
||||
if callable(popular_getter):
|
||||
titles = list(popular_getter() or [])
|
||||
titles = _run_with_progress(
|
||||
POPULAR_MENU_LABEL,
|
||||
f"{plugin_name}: Liste wird geladen...",
|
||||
lambda: list(popular_getter() or []),
|
||||
)
|
||||
else:
|
||||
label = _popular_genre_label(plugin)
|
||||
if not label:
|
||||
titles = []
|
||||
else:
|
||||
titles = list(plugin.titles_for_genre(label) or [])
|
||||
titles = _run_with_progress(
|
||||
POPULAR_MENU_LABEL,
|
||||
f"{plugin_name}: Liste wird geladen...",
|
||||
lambda: list(plugin.titles_for_genre(label) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Beliebte Serien konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Beliebte Serien", "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
_log(f"{POPULAR_MENU_LABEL} konnte nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
@@ -2630,7 +2612,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
total = len(titles)
|
||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||
page = min(page, total_pages)
|
||||
xbmcplugin.setPluginCategory(handle, f"Beliebte Serien [{plugin_name}] ({page}/{total_pages})")
|
||||
xbmcplugin.setPluginCategory(handle, f"{POPULAR_MENU_LABEL} [{plugin_name}] ({page}/{total_pages})")
|
||||
_set_content(handle, "tvshows")
|
||||
|
||||
if total_pages > 1 and page > 1:
|
||||
@@ -2664,7 +2646,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
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("Beliebte Titel werden geladen..."):
|
||||
with _busy_dialog(f"{POPULAR_MENU_LABEL} wird geladen..."):
|
||||
tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles)
|
||||
for title in page_items:
|
||||
tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, [])
|
||||
@@ -2691,7 +2673,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
if total_pages > 1 and page < total_pages:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"popular",
|
||||
{"plugin": plugin_name, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -2701,15 +2683,15 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
|
||||
sources = _plugins_with_popular()
|
||||
if not sources:
|
||||
xbmcgui.Dialog().notification("Beliebte Serien", "Keine Quellen gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Keine Quellen gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
xbmcplugin.setPluginCategory(handle, "Beliebte Serien")
|
||||
xbmcplugin.setPluginCategory(handle, POPULAR_MENU_LABEL)
|
||||
for name, plugin, _label in sources:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
f"Beliebte Serien [{plugin.name}]",
|
||||
f"{POPULAR_MENU_LABEL} [{plugin.name}]",
|
||||
"popular",
|
||||
{"plugin": name, "page": "1"},
|
||||
is_folder=True,
|
||||
@@ -2717,7 +2699,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
|
||||
|
||||
def _show_new_titles(plugin_name: str, 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 = max(1, int(page or 1))
|
||||
@@ -2725,13 +2707,13 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
|
||||
plugin_name = (plugin_name or "").strip()
|
||||
plugin = _discover_plugins().get(plugin_name)
|
||||
if plugin is None or not _plugin_has_capability(plugin, "new_titles"):
|
||||
xbmcgui.Dialog().notification("Neue Titel", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
getter = getattr(plugin, "new_titles", None)
|
||||
if not callable(getter):
|
||||
xbmcgui.Dialog().notification("Neue Titel", "Diese Liste ist nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Liste ist nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
@@ -2739,31 +2721,39 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
|
||||
has_more_getter = getattr(plugin, "new_titles_has_more", None)
|
||||
|
||||
if callable(paging_getter):
|
||||
xbmcplugin.setPluginCategory(handle, f"Neue Titel [{plugin_name}] ({page})")
|
||||
xbmcplugin.setPluginCategory(handle, f"{LATEST_MENU_LABEL} [{plugin_name}] ({page})")
|
||||
_set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows")
|
||||
if page > 1:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Vorherige Seite",
|
||||
"new_titles",
|
||||
action_name,
|
||||
{"plugin": plugin_name, "page": str(page - 1)},
|
||||
is_folder=True,
|
||||
)
|
||||
try:
|
||||
page_items = list(paging_getter(page) or [])
|
||||
page_items = _run_with_progress(
|
||||
LATEST_MENU_LABEL,
|
||||
f"{plugin_name}: Seite {page} wird geladen...",
|
||||
lambda: list(paging_getter(page) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Neue Titel konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Neue Titel", "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
_log(f"{LATEST_MENU_LABEL} konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
page_items = [str(t).strip() for t in page_items if t and str(t).strip()]
|
||||
page_items.sort(key=lambda value: value.casefold())
|
||||
else:
|
||||
try:
|
||||
titles = list(getter() or [])
|
||||
titles = _run_with_progress(
|
||||
LATEST_MENU_LABEL,
|
||||
f"{plugin_name}: Liste wird geladen...",
|
||||
lambda: list(getter() or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Neue Titel konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Neue Titel", "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
_log(f"{LATEST_MENU_LABEL} konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
@@ -2772,21 +2762,21 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
|
||||
total = len(titles)
|
||||
if total == 0:
|
||||
xbmcgui.Dialog().notification(
|
||||
"Neue Titel",
|
||||
LATEST_MENU_LABEL,
|
||||
"Keine Titel gefunden. Bitte Basis-URL oder Index pruefen.",
|
||||
xbmcgui.NOTIFICATION_INFO,
|
||||
4000,
|
||||
)
|
||||
total_pages = max(1, (total + page_size - 1) // page_size)
|
||||
page = min(page, total_pages)
|
||||
xbmcplugin.setPluginCategory(handle, f"Neue Titel [{plugin_name}] ({page}/{total_pages})")
|
||||
xbmcplugin.setPluginCategory(handle, f"{LATEST_MENU_LABEL} [{plugin_name}] ({page}/{total_pages})")
|
||||
_set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows")
|
||||
|
||||
if total_pages > 1 and page > 1:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Vorherige Seite",
|
||||
"new_titles",
|
||||
action_name,
|
||||
{"plugin": plugin_name, "page": str(page - 1)},
|
||||
is_folder=True,
|
||||
)
|
||||
@@ -2812,7 +2802,7 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
|
||||
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("Neue Titel werden geladen..."):
|
||||
with _busy_dialog(f"{LATEST_MENU_LABEL} wird geladen..."):
|
||||
tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles)
|
||||
for title in page_items:
|
||||
tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, [])
|
||||
@@ -2851,8 +2841,8 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"new_titles",
|
||||
"Naechste Seite",
|
||||
action_name,
|
||||
{"plugin": plugin_name, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
)
|
||||
@@ -2864,25 +2854,28 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None:
|
||||
plugin_name = (plugin_name or "").strip()
|
||||
plugin = _discover_plugins().get(plugin_name)
|
||||
if not plugin:
|
||||
xbmcgui.Dialog().notification("Neueste Folgen", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
getter = getattr(plugin, "latest_episodes", None)
|
||||
if not callable(getter):
|
||||
xbmcgui.Dialog().notification("Neueste Folgen", "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
xbmcplugin.setPluginCategory(handle, f"{plugin_name}: Neueste Folgen")
|
||||
xbmcplugin.setPluginCategory(handle, f"{plugin_name}: {LATEST_MENU_LABEL}")
|
||||
_set_content(handle, "episodes")
|
||||
|
||||
try:
|
||||
with _busy_dialog("Neueste Episoden werden geladen..."):
|
||||
entries = list(getter(page) or [])
|
||||
entries = _run_with_progress(
|
||||
LATEST_MENU_LABEL,
|
||||
f"{plugin_name}: Seite {page} wird geladen...",
|
||||
lambda: list(getter(page) or []),
|
||||
)
|
||||
except Exception as exc:
|
||||
_log(f"Neueste Folgen fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Neueste Folgen", "Abruf fehlgeschlagen.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
_log(f"{LATEST_MENU_LABEL} fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Abruf fehlgeschlagen.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
|
||||
@@ -2935,6 +2928,25 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None:
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
|
||||
|
||||
def _show_latest_titles(plugin_name: str, page: int = 1) -> None:
|
||||
plugin_name = (plugin_name or "").strip()
|
||||
plugin = _discover_plugins().get(plugin_name)
|
||||
if plugin is None:
|
||||
handle = _get_handle()
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
return
|
||||
if _plugin_has_capability(plugin, "latest_episodes"):
|
||||
_show_latest_episodes(plugin_name, page)
|
||||
return
|
||||
if _plugin_has_capability(plugin, "new_titles"):
|
||||
_show_new_titles(plugin_name, page, action_name="latest_titles")
|
||||
return
|
||||
handle = _get_handle()
|
||||
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
xbmcplugin.endOfDirectory(handle)
|
||||
|
||||
|
||||
def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page: int = 1) -> None:
|
||||
handle = _get_handle()
|
||||
page_size = 10
|
||||
@@ -2949,7 +2961,12 @@ def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page
|
||||
grouped_has_more = getattr(plugin, "genre_group_has_more", None)
|
||||
if callable(grouped_paging):
|
||||
try:
|
||||
page_items = [str(t).strip() for t in list(grouped_paging(genre, group_code, page, page_size) or []) if t and str(t).strip()]
|
||||
raw_items = _run_with_progress(
|
||||
"Genres",
|
||||
f"{plugin_name}: {genre} [{group_code}] Seite {page} wird geladen...",
|
||||
lambda: list(grouped_paging(genre, group_code, page, page_size) or []),
|
||||
)
|
||||
page_items = [str(t).strip() for t in raw_items if t and str(t).strip()]
|
||||
except Exception as exc:
|
||||
_log(f"Genre-Serien konnten nicht geladen werden ({plugin_name}/{genre}/{group_code} p{page}): {exc}", xbmc.LOGWARNING)
|
||||
xbmcgui.Dialog().notification("Genres", "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||
@@ -3018,7 +3035,7 @@ def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page
|
||||
if show_next:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"genre_series_group",
|
||||
{"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -3099,7 +3116,7 @@ def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page
|
||||
if total_pages > 1 and page < total_pages:
|
||||
_add_directory_item(
|
||||
handle,
|
||||
"Nächste Seite",
|
||||
"Naechste Seite",
|
||||
"genre_series_group",
|
||||
{"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)},
|
||||
is_folder=True,
|
||||
@@ -3115,27 +3132,44 @@ def _open_settings() -> None:
|
||||
addon.openSettings()
|
||||
|
||||
|
||||
def _run_update_check() -> None:
|
||||
"""Stoesst Kodi-Repo- und Addon-Updates an und informiert den Benutzer."""
|
||||
def _run_update_check(*, silent: bool = False) -> None:
|
||||
"""Stoesst Kodi-Repo- und Addon-Updates an."""
|
||||
if xbmc is None: # pragma: no cover - outside Kodi
|
||||
return
|
||||
try:
|
||||
info_url = _normalize_update_info_url(_get_setting_string("update_repo_url"))
|
||||
_set_setting_string("update_repo_url", info_url)
|
||||
info_url = _resolve_update_info_url()
|
||||
_sync_update_version_settings()
|
||||
_update_repository_source(info_url)
|
||||
builtin = getattr(xbmc, "executebuiltin", None)
|
||||
if callable(builtin):
|
||||
builtin("UpdateAddonRepos")
|
||||
builtin("UpdateLocalAddons")
|
||||
builtin("ActivateWindow(addonbrowser,addons://updates/)")
|
||||
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000)
|
||||
if not silent:
|
||||
builtin("ActivateWindow(addonbrowser,addons://updates/)")
|
||||
if not silent:
|
||||
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000)
|
||||
except Exception as exc:
|
||||
_log(f"Update-Pruefung fehlgeschlagen: {exc}", xbmc.LOGWARNING)
|
||||
try:
|
||||
xbmcgui.Dialog().notification("Updates", "Update-Check fehlgeschlagen.", xbmcgui.NOTIFICATION_ERROR, 4000)
|
||||
except Exception:
|
||||
pass
|
||||
if not silent:
|
||||
try:
|
||||
xbmcgui.Dialog().notification("Updates", "Update-Check fehlgeschlagen.", xbmcgui.NOTIFICATION_ERROR, 4000)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
def _maybe_run_auto_update_check(action: str | None) -> None:
|
||||
action = (action or "").strip()
|
||||
# Auto-Check nur beim Root-Menue, nicht in jedem Untermenue.
|
||||
if action:
|
||||
return
|
||||
if not _get_setting_bool("auto_update_enabled", default=False):
|
||||
return
|
||||
now = int(time.time())
|
||||
last = _get_setting_int("auto_update_last_ts", default=0)
|
||||
if last > 0 and (now - last) < AUTO_UPDATE_INTERVAL_SEC:
|
||||
return
|
||||
_set_setting_string("auto_update_last_ts", str(now))
|
||||
_run_update_check(silent=True)
|
||||
|
||||
|
||||
def _extract_first_int(value: str) -> int | None:
|
||||
@@ -3209,81 +3243,12 @@ def _play_final_link(
|
||||
|
||||
|
||||
def _track_playback_and_update_state(key: str) -> None:
|
||||
if not key:
|
||||
return
|
||||
monitor = xbmc.Monitor() if xbmc is not None and hasattr(xbmc, "Monitor") else None
|
||||
player = xbmc.Player()
|
||||
|
||||
# Wait for playback start.
|
||||
started = False
|
||||
for _ in range(30):
|
||||
try:
|
||||
if player.isPlayingVideo():
|
||||
started = True
|
||||
break
|
||||
except Exception:
|
||||
pass
|
||||
if monitor and monitor.waitForAbort(0.5):
|
||||
return
|
||||
if not started:
|
||||
return
|
||||
|
||||
last_pos = 0.0
|
||||
total = 0.0
|
||||
while True:
|
||||
try:
|
||||
if not player.isPlayingVideo():
|
||||
break
|
||||
last_pos = float(player.getTime() or 0.0)
|
||||
total = float(player.getTotalTime() or 0.0)
|
||||
except Exception:
|
||||
pass
|
||||
if monitor and monitor.waitForAbort(1.0):
|
||||
return
|
||||
|
||||
if total <= 0.0:
|
||||
return
|
||||
percent = max(0.0, min(1.0, last_pos / total))
|
||||
state: dict[str, object] = {"last_position": int(last_pos), "resume_total": int(total), "percent": percent}
|
||||
if percent >= WATCHED_THRESHOLD:
|
||||
state["watched"] = True
|
||||
state["resume_position"] = 0
|
||||
elif last_pos > 0:
|
||||
state["watched"] = False
|
||||
state["resume_position"] = int(last_pos)
|
||||
_set_playstate(key, state)
|
||||
|
||||
# Zusätzlich aggregiert speichern, damit Titel-/Staffel-Listen "gesehen/fortsetzen"
|
||||
# anzeigen können (für Filme/Serien gleichermaßen).
|
||||
try:
|
||||
parts = str(key).split("\t")
|
||||
if len(parts) == 4:
|
||||
plugin_name, title, season, _episode = parts
|
||||
plugin_name = (plugin_name or "").strip()
|
||||
title = (title or "").strip()
|
||||
season = (season or "").strip()
|
||||
if plugin_name and title:
|
||||
_set_playstate(_playstate_key(plugin_name=plugin_name, title=title, season="", episode=""), state)
|
||||
if season:
|
||||
_set_playstate(_playstate_key(plugin_name=plugin_name, title=title, season=season, episode=""), state)
|
||||
except Exception:
|
||||
pass
|
||||
return
|
||||
|
||||
|
||||
def _track_playback_and_update_state_async(key: str) -> None:
|
||||
"""Startet Playstate-Tracking im Hintergrund, damit die UI nicht blockiert."""
|
||||
key = (key or "").strip()
|
||||
if not key:
|
||||
return
|
||||
|
||||
def _worker() -> None:
|
||||
try:
|
||||
_track_playback_and_update_state(key)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
worker = threading.Thread(target=_worker, name="viewit-playstate-tracker", daemon=True)
|
||||
worker.start()
|
||||
# Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst.
|
||||
return
|
||||
|
||||
|
||||
def _play_episode(
|
||||
@@ -3370,6 +3335,7 @@ def _play_episode(
|
||||
return
|
||||
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
||||
final_link = plugin.resolve_stream_link(link) or link
|
||||
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
||||
finally:
|
||||
if restore_hosters is not None and callable(preferred_setter):
|
||||
preferred_setter(restore_hosters)
|
||||
@@ -3457,6 +3423,7 @@ def _play_episode_url(
|
||||
return
|
||||
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
||||
final_link = plugin.resolve_stream_link(link) or link
|
||||
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
||||
finally:
|
||||
if restore_hosters is not None and callable(preferred_setter):
|
||||
preferred_setter(restore_hosters)
|
||||
@@ -3496,6 +3463,7 @@ def run() -> None:
|
||||
params = _parse_params()
|
||||
action = params.get("action")
|
||||
_log(f"Action: {action}", xbmc.LOGDEBUG)
|
||||
_maybe_run_auto_update_check(action)
|
||||
if action == "search":
|
||||
_show_search()
|
||||
elif action == "plugin_menu":
|
||||
@@ -3508,6 +3476,11 @@ def run() -> None:
|
||||
_show_genres(params.get("plugin", ""))
|
||||
elif action == "categories":
|
||||
_show_categories(params.get("plugin", ""))
|
||||
elif action == "latest_titles":
|
||||
_show_latest_titles(
|
||||
params.get("plugin", ""),
|
||||
_parse_positive_int(params.get("page", "1"), default=1),
|
||||
)
|
||||
elif action == "new_titles":
|
||||
_show_new_titles(
|
||||
params.get("plugin", ""),
|
||||
|
||||
Reference in New Issue
Block a user