dev: umfangreiches Refactoring, Trakt-Integration und Code-Review-Fixes (0.1.69-dev)
Core & Architektur: - Neues Verzeichnis addon/core/ mit router.py, trakt.py, metadata.py, gui.py, playstate.py, plugin_manager.py, updater.py - Tests-Verzeichnis hinzugefügt (24 Tests, pytest + Coverage) Trakt-Integration: - OAuth Device Flow, Scrobbling, Watchlist, History, Calendar - Upcoming Episodes, Weiterschauen (Continue Watching) - Watched-Status in Episodenlisten - _trakt_find_in_plugins() mit 5-Min-Cache Serienstream-Suche: - API-Ergebnisse werden immer mit Katalog-Cache ergänzt (serverseitiges 10-Treffer-Limit) - Katalog-Cache wird beim Addon-Start im Daemon-Thread vorgewärmt - Notification nach Cache-Load via xbmc.executebuiltin() (thread-sicher) Bugfixes (Code-Review): - Race Condition auf _TRAKT_WATCHED_CACHE: _TRAKT_WATCHED_CACHE_LOCK hinzugefügt - GUI-Dialog aus Daemon-Thread: xbmcgui -> xbmc.executebuiltin() - ValueError in Trakt-Watchlist-Routen abgesichert - Token expires_at==0 Check korrigiert - get_setting_bool() Kontrollfluss in gui.py bereinigt - topstreamfilm_plugin: try-finally um xbmcvfs.File.close() Cleanup: - default.py.bak und refactor_router.py entfernt - .gitignore: /tests/ Eintrag entfernt - Type-Hints vereinheitlicht (Dict/List/Tuple -> dict/list/tuple)
This commit is contained in:
738
addon/core/updater.py
Normal file
738
addon/core/updater.py
Normal file
@@ -0,0 +1,738 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Update- und Versionsverwaltung fuer ViewIT.
|
||||
|
||||
Dieses Modul kuemmert sich um:
|
||||
- Update-Kanaele (Main, Nightly, Dev, Custom)
|
||||
- Versions-Abfrage und -Installation aus Repositories
|
||||
- Changelog-Abruf
|
||||
- Repository-Quellen-Verwaltung
|
||||
- ResolveURL Auto-Installation
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import io
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
import xml.etree.ElementTree as ET
|
||||
import zipfile
|
||||
from urllib.error import URLError
|
||||
from urllib.request import Request, urlopen
|
||||
|
||||
try: # pragma: no cover - Kodi runtime
|
||||
import xbmc # type: ignore[import-not-found]
|
||||
import xbmcaddon # type: ignore[import-not-found]
|
||||
import xbmcgui # type: ignore[import-not-found]
|
||||
import xbmcvfs # type: ignore[import-not-found]
|
||||
except ImportError: # pragma: no cover - allow importing outside Kodi
|
||||
xbmc = None
|
||||
xbmcaddon = None
|
||||
xbmcgui = None
|
||||
xbmcvfs = None
|
||||
|
||||
from plugin_helpers import show_error, show_notification
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Konstanten
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
UPDATE_CHANNEL_MAIN = 0
|
||||
UPDATE_CHANNEL_NIGHTLY = 1
|
||||
UPDATE_CHANNEL_CUSTOM = 2
|
||||
UPDATE_CHANNEL_DEV = 3
|
||||
AUTO_UPDATE_INTERVAL_SEC = 6 * 60 * 60
|
||||
UPDATE_HTTP_TIMEOUT_SEC = 8
|
||||
UPDATE_ADDON_ID = "plugin.video.viewit"
|
||||
RESOLVEURL_ADDON_ID = "script.module.resolveurl"
|
||||
RESOLVEURL_AUTO_INSTALL_INTERVAL_SEC = 6 * 60 * 60
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Hilfsfunktionen (Settings-Zugriff)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
# Diese Callbacks werden von default.py einmal gesetzt, damit updater.py
|
||||
# keine zirkulaeren Abhaengigkeiten hat.
|
||||
_get_setting_string = None
|
||||
_get_setting_bool = None
|
||||
_get_setting_int = None
|
||||
_set_setting_string = None
|
||||
_get_addon = None
|
||||
_log_fn = None
|
||||
|
||||
|
||||
def init(
|
||||
*,
|
||||
get_setting_string,
|
||||
get_setting_bool,
|
||||
get_setting_int,
|
||||
set_setting_string,
|
||||
get_addon,
|
||||
log_fn,
|
||||
) -> None:
|
||||
"""Initialisiert Callbacks fuer Settings-Zugriff."""
|
||||
global _get_setting_string, _get_setting_bool, _get_setting_int
|
||||
global _set_setting_string, _get_addon, _log_fn
|
||||
_get_setting_string = get_setting_string
|
||||
_get_setting_bool = get_setting_bool
|
||||
_get_setting_int = get_setting_int
|
||||
_set_setting_string = set_setting_string
|
||||
_get_addon = get_addon
|
||||
_log_fn = log_fn
|
||||
|
||||
|
||||
def _log(message: str, level: int = 1) -> None:
|
||||
if _log_fn is not None:
|
||||
_log_fn(message, level)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# URL-Normalisierung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def normalize_update_info_url(raw: str) -> str:
|
||||
value = str(raw or "").strip()
|
||||
default = "http://127.0.0.1:8080/repo/addons.xml"
|
||||
if not value:
|
||||
return default
|
||||
if value.endswith("/addons.xml"):
|
||||
return value
|
||||
return value.rstrip("/") + "/addons.xml"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Update-Kanaele
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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, UPDATE_CHANNEL_DEV}:
|
||||
return UPDATE_CHANNEL_MAIN
|
||||
return channel
|
||||
|
||||
|
||||
def channel_label(channel: int) -> str:
|
||||
if channel == UPDATE_CHANNEL_NIGHTLY:
|
||||
return "Nightly"
|
||||
if channel == UPDATE_CHANNEL_DEV:
|
||||
return "Dev"
|
||||
if channel == UPDATE_CHANNEL_CUSTOM:
|
||||
return "Custom"
|
||||
return "Main"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Versionierung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def version_sort_key(version: str) -> tuple[int, ...]:
|
||||
base = str(version or "").split("-", 1)[0]
|
||||
parts = []
|
||||
for chunk in base.split("."):
|
||||
try:
|
||||
parts.append(int(chunk))
|
||||
except Exception:
|
||||
parts.append(0)
|
||||
while len(parts) < 4:
|
||||
parts.append(0)
|
||||
return tuple(parts[:4])
|
||||
|
||||
|
||||
def is_stable_version(version: str) -> bool:
|
||||
return bool(re.match(r"^\d+\.\d+\.\d+$", str(version or "").strip()))
|
||||
|
||||
|
||||
def is_nightly_version(version: str) -> bool:
|
||||
return bool(re.match(r"^\d+\.\d+\.\d+-nightly$", str(version or "").strip()))
|
||||
|
||||
|
||||
def is_dev_version(version: str) -> bool:
|
||||
return bool(re.match(r"^\d+\.\d+\.\d+-dev$", str(version or "").strip()))
|
||||
|
||||
|
||||
def filter_versions_for_channel(channel: int, versions: list[str]) -> list[str]:
|
||||
if channel == UPDATE_CHANNEL_MAIN:
|
||||
return [v for v in versions if is_stable_version(v)]
|
||||
if channel == UPDATE_CHANNEL_NIGHTLY:
|
||||
return [v for v in versions if is_nightly_version(v)]
|
||||
if channel == UPDATE_CHANNEL_DEV:
|
||||
return [v for v in versions if is_dev_version(v)]
|
||||
return list(versions)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# HTTP-Helfer
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def read_text_url(url: str, *, timeout: int = UPDATE_HTTP_TIMEOUT_SEC) -> str:
|
||||
request = Request(url, headers={"User-Agent": "ViewIT/1.0"})
|
||||
response = None
|
||||
try:
|
||||
response = urlopen(request, timeout=timeout)
|
||||
data = response.read()
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
return data.decode("utf-8", errors="replace")
|
||||
|
||||
|
||||
def read_binary_url(url: str, *, timeout: int = UPDATE_HTTP_TIMEOUT_SEC) -> bytes:
|
||||
request = Request(url, headers={"User-Agent": "ViewIT/1.0"})
|
||||
response = None
|
||||
try:
|
||||
response = urlopen(request, timeout=timeout)
|
||||
return response.read()
|
||||
finally:
|
||||
if response is not None:
|
||||
try:
|
||||
response.close()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repo-Abfragen
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def extract_repo_addon_version(xml_text: str, addon_id: str = UPDATE_ADDON_ID) -> str:
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except Exception:
|
||||
return "-"
|
||||
if root.tag == "addon":
|
||||
return str(root.attrib.get("version") or "-")
|
||||
for node in root.findall("addon"):
|
||||
if str(node.attrib.get("id") or "").strip() == addon_id:
|
||||
version = str(node.attrib.get("version") or "").strip()
|
||||
return version or "-"
|
||||
return "-"
|
||||
|
||||
|
||||
def fetch_repo_addon_version(info_url: str) -> str:
|
||||
url = normalize_update_info_url(info_url)
|
||||
try:
|
||||
xml_text = read_text_url(url)
|
||||
except URLError:
|
||||
return "-"
|
||||
except Exception:
|
||||
return "-"
|
||||
return extract_repo_addon_version(xml_text)
|
||||
|
||||
|
||||
def _extract_repo_identity(info_url: str) -> tuple[str, str, str, str] | None:
|
||||
from urllib.parse import urlparse
|
||||
|
||||
parsed = urlparse(str(info_url or "").strip())
|
||||
parts = [part for part in parsed.path.split("/") if part]
|
||||
try:
|
||||
raw_idx = parts.index("raw")
|
||||
except ValueError:
|
||||
return None
|
||||
if raw_idx < 2 or (raw_idx + 2) >= len(parts):
|
||||
return None
|
||||
if parts[raw_idx + 1] != "branch":
|
||||
return None
|
||||
owner = parts[raw_idx - 2]
|
||||
repo = parts[raw_idx - 1]
|
||||
branch = parts[raw_idx + 2]
|
||||
scheme = parsed.scheme or "https"
|
||||
host = parsed.netloc
|
||||
if not owner or not repo or not branch or not host:
|
||||
return None
|
||||
return scheme, host, owner, repo + "|" + branch
|
||||
|
||||
|
||||
def fetch_repo_versions(info_url: str) -> list[str]:
|
||||
identity = _extract_repo_identity(info_url)
|
||||
if identity is None:
|
||||
one = fetch_repo_addon_version(info_url)
|
||||
return [one] if one != "-" else []
|
||||
|
||||
scheme, host, owner, repo_branch = identity
|
||||
repo, branch = repo_branch.split("|", 1)
|
||||
api_url = f"{scheme}://{host}/api/v1/repos/{owner}/{repo}/contents/{UPDATE_ADDON_ID}?ref={branch}"
|
||||
|
||||
try:
|
||||
payload = read_text_url(api_url)
|
||||
data = json.loads(payload)
|
||||
except Exception:
|
||||
one = fetch_repo_addon_version(info_url)
|
||||
return [one] if one != "-" else []
|
||||
|
||||
versions: list[str] = []
|
||||
if isinstance(data, list):
|
||||
for entry in data:
|
||||
if not isinstance(entry, dict):
|
||||
continue
|
||||
name = str(entry.get("name") or "")
|
||||
match = re.match(rf"^{re.escape(UPDATE_ADDON_ID)}-(.+)\.zip$", name)
|
||||
if not match:
|
||||
continue
|
||||
version = match.group(1).strip()
|
||||
if version:
|
||||
versions.append(version)
|
||||
unique = sorted(set(versions), key=version_sort_key, reverse=True)
|
||||
return unique
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Changelog
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def extract_changelog_section(changelog_text: str, version: str) -> str:
|
||||
lines = changelog_text.splitlines()
|
||||
wanted = (version or "").strip()
|
||||
if not wanted:
|
||||
return "\n".join(lines[:120]).strip()
|
||||
|
||||
start = -1
|
||||
for idx, line in enumerate(lines):
|
||||
if line.startswith("## ") and wanted in line:
|
||||
start = idx
|
||||
break
|
||||
if start < 0:
|
||||
return f"Kein Changelog-Abschnitt fuer Version {wanted} gefunden."
|
||||
|
||||
end = len(lines)
|
||||
for idx in range(start + 1, len(lines)):
|
||||
if lines[idx].startswith("## "):
|
||||
end = idx
|
||||
break
|
||||
return "\n".join(lines[start:end]).strip()
|
||||
|
||||
|
||||
def fetch_changelog_for_channel(channel: int, version: str) -> str:
|
||||
version_text = str(version or "").strip().casefold()
|
||||
if version_text.endswith("-dev"):
|
||||
url = "https://gitea.it-drui.de/viewit/ViewIT/raw/branch/dev/CHANGELOG-DEV.md"
|
||||
elif version_text.endswith("-nightly"):
|
||||
url = "https://gitea.it-drui.de/viewit/ViewIT/raw/branch/nightly/CHANGELOG-NIGHTLY.md"
|
||||
elif channel == UPDATE_CHANNEL_DEV:
|
||||
url = "https://gitea.it-drui.de/viewit/ViewIT/raw/branch/dev/CHANGELOG-DEV.md"
|
||||
elif channel == UPDATE_CHANNEL_MAIN:
|
||||
url = "https://gitea.it-drui.de/viewit/ViewIT/raw/branch/main/CHANGELOG.md"
|
||||
else:
|
||||
url = "https://gitea.it-drui.de/viewit/ViewIT/raw/branch/nightly/CHANGELOG-NIGHTLY.md"
|
||||
try:
|
||||
text = read_text_url(url)
|
||||
except Exception:
|
||||
return "Changelog konnte nicht geladen werden."
|
||||
return extract_changelog_section(text, version)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def install_addon_version_manual(info_url: str, version: str) -> bool:
|
||||
base = info_url[: -len("/addons.xml")] if info_url.endswith("/addons.xml") else info_url.rstrip("/")
|
||||
zip_url = f"{base}/{UPDATE_ADDON_ID}/{UPDATE_ADDON_ID}-{version}.zip"
|
||||
try:
|
||||
zip_bytes = read_binary_url(zip_url)
|
||||
except Exception as exc:
|
||||
_log(f"Download fehlgeschlagen ({zip_url}): {exc}", 2)
|
||||
return False
|
||||
|
||||
if xbmcvfs is None:
|
||||
return False
|
||||
|
||||
addons_root = xbmcvfs.translatePath("special://home/addons")
|
||||
addons_root_real = os.path.realpath(addons_root)
|
||||
try:
|
||||
with zipfile.ZipFile(io.BytesIO(zip_bytes)) as archive:
|
||||
for member in archive.infolist():
|
||||
name = str(member.filename or "")
|
||||
if not name or name.endswith("/"):
|
||||
continue
|
||||
target = os.path.realpath(os.path.join(addons_root, name))
|
||||
if not target.startswith(addons_root_real + os.sep):
|
||||
_log(f"Sicherheitswarnung: Verdaechtiger ZIP-Eintrag abgelehnt: {name!r}", 2)
|
||||
return False
|
||||
os.makedirs(os.path.dirname(target), exist_ok=True)
|
||||
with archive.open(member, "r") as src, open(target, "wb") as dst:
|
||||
dst.write(src.read())
|
||||
except Exception as exc:
|
||||
_log(f"Entpacken fehlgeschlagen: {exc}", 2)
|
||||
return False
|
||||
|
||||
builtin = getattr(xbmc, "executebuiltin", None) if xbmc else None
|
||||
if callable(builtin):
|
||||
builtin("UpdateLocalAddons")
|
||||
return True
|
||||
|
||||
|
||||
def install_addon_version(info_url: str, version: str) -> bool:
|
||||
base = info_url[: -len("/addons.xml")] if info_url.endswith("/addons.xml") else info_url.rstrip("/")
|
||||
zip_url = f"{base}/{UPDATE_ADDON_ID}/{UPDATE_ADDON_ID}-{version}.zip"
|
||||
|
||||
builtin = getattr(xbmc, "executebuiltin", None) if xbmc else None
|
||||
if callable(builtin):
|
||||
try:
|
||||
before = installed_addon_version_from_disk()
|
||||
builtin(f"InstallAddon({zip_url})")
|
||||
for _ in range(20):
|
||||
time.sleep(1)
|
||||
current = installed_addon_version_from_disk()
|
||||
if current == version:
|
||||
return True
|
||||
if before == version:
|
||||
return True
|
||||
except Exception as exc:
|
||||
_log(f"InstallAddon fehlgeschlagen, fallback aktiv: {exc}", 2)
|
||||
|
||||
return install_addon_version_manual(info_url, version)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Installierte Version / Addon-Pruefung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def installed_addon_version_from_disk() -> str:
|
||||
if xbmcvfs is None:
|
||||
return "0.0.0"
|
||||
try:
|
||||
addon_xml = xbmcvfs.translatePath(f"special://home/addons/{UPDATE_ADDON_ID}/addon.xml")
|
||||
except Exception:
|
||||
return "0.0.0"
|
||||
if not addon_xml or not os.path.exists(addon_xml):
|
||||
return "0.0.0"
|
||||
try:
|
||||
root = ET.parse(addon_xml).getroot()
|
||||
version = str(root.attrib.get("version") or "").strip()
|
||||
return version or "0.0.0"
|
||||
except Exception:
|
||||
return "0.0.0"
|
||||
|
||||
|
||||
def is_addon_installed(addon_id: str) -> bool:
|
||||
addon_id = str(addon_id or "").strip()
|
||||
if not addon_id:
|
||||
return False
|
||||
has_addon = getattr(xbmc, "getCondVisibility", None) if xbmc else None
|
||||
if callable(has_addon):
|
||||
try:
|
||||
return bool(has_addon(f"System.HasAddon({addon_id})"))
|
||||
except Exception:
|
||||
pass
|
||||
if xbmcvfs is None:
|
||||
return False
|
||||
try:
|
||||
addon_xml = xbmcvfs.translatePath(f"special://home/addons/{addon_id}/addon.xml")
|
||||
except Exception:
|
||||
return False
|
||||
return bool(addon_xml and os.path.exists(addon_xml))
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Repository-Quellen-Verwaltung
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def repo_addon_xml_path() -> str:
|
||||
if xbmcvfs is None:
|
||||
return ""
|
||||
try:
|
||||
return xbmcvfs.translatePath("special://home/addons/repository.viewit/addon.xml")
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
|
||||
def update_repository_source(info_url: str) -> bool:
|
||||
path = repo_addon_xml_path()
|
||||
if not path:
|
||||
return False
|
||||
if not os.path.exists(path):
|
||||
return False
|
||||
try:
|
||||
tree = ET.parse(path)
|
||||
root = tree.getroot()
|
||||
dir_node = root.find(".//dir")
|
||||
if dir_node is None:
|
||||
return False
|
||||
info = dir_node.find("info")
|
||||
checksum = dir_node.find("checksum")
|
||||
datadir = dir_node.find("datadir")
|
||||
if info is None or checksum is None or datadir is None:
|
||||
return False
|
||||
base = info_url[: -len("/addons.xml")] if info_url.endswith("/addons.xml") else info_url.rstrip("/")
|
||||
info.text = info_url
|
||||
checksum.text = f"{base}/addons.xml.md5"
|
||||
datadir.text = f"{base}/"
|
||||
tree.write(path, encoding="utf-8", xml_declaration=True)
|
||||
return True
|
||||
except Exception as exc:
|
||||
_log(f"Repository-URL konnte nicht gesetzt werden: {exc}", 2)
|
||||
return False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# ResolveURL
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def sync_resolveurl_status_setting() -> None:
|
||||
status = "Installiert" if is_addon_installed(RESOLVEURL_ADDON_ID) else "Fehlt"
|
||||
_set_setting_string("resolveurl_status", status)
|
||||
|
||||
|
||||
def install_kodi_addon(addon_id: str, *, wait_seconds: int) -> bool:
|
||||
if is_addon_installed(addon_id):
|
||||
return True
|
||||
builtin = getattr(xbmc, "executebuiltin", None) if xbmc else None
|
||||
if not callable(builtin):
|
||||
return False
|
||||
try:
|
||||
builtin(f"InstallAddon({addon_id})")
|
||||
builtin("UpdateLocalAddons")
|
||||
except Exception as exc:
|
||||
_log(f"InstallAddon fehlgeschlagen ({addon_id}): {exc}", 2)
|
||||
return False
|
||||
|
||||
if wait_seconds <= 0:
|
||||
return is_addon_installed(addon_id)
|
||||
deadline = time.time() + max(1, int(wait_seconds))
|
||||
while time.time() < deadline:
|
||||
if is_addon_installed(addon_id):
|
||||
return True
|
||||
time.sleep(1)
|
||||
return is_addon_installed(addon_id)
|
||||
|
||||
|
||||
def ensure_resolveurl_installed(*, force: bool, silent: bool) -> bool:
|
||||
if is_addon_installed(RESOLVEURL_ADDON_ID):
|
||||
sync_resolveurl_status_setting()
|
||||
return True
|
||||
if not force and not _get_setting_bool("resolveurl_auto_install", default=True):
|
||||
sync_resolveurl_status_setting()
|
||||
return False
|
||||
|
||||
now = int(time.time())
|
||||
if not force:
|
||||
last_try = _get_setting_int("resolveurl_last_ts", default=0)
|
||||
if last_try > 0 and (now - last_try) < RESOLVEURL_AUTO_INSTALL_INTERVAL_SEC:
|
||||
return False
|
||||
_set_setting_string("resolveurl_last_ts", str(now))
|
||||
|
||||
wait_seconds = 20 if force else 0
|
||||
ok = install_kodi_addon(RESOLVEURL_ADDON_ID, wait_seconds=wait_seconds)
|
||||
sync_resolveurl_status_setting()
|
||||
|
||||
if not silent and xbmcgui is not None:
|
||||
if ok:
|
||||
xbmcgui.Dialog().notification(
|
||||
"ResolveURL",
|
||||
"script.module.resolveurl ist installiert.",
|
||||
xbmcgui.NOTIFICATION_INFO,
|
||||
4000,
|
||||
)
|
||||
else:
|
||||
xbmcgui.Dialog().notification(
|
||||
"ResolveURL",
|
||||
"Installation fehlgeschlagen. Bitte Repository/Netzwerk pruefen.",
|
||||
xbmcgui.NOTIFICATION_ERROR,
|
||||
5000,
|
||||
)
|
||||
return ok
|
||||
|
||||
|
||||
def maybe_auto_install_resolveurl(action: str | None) -> None:
|
||||
if (action or "").strip():
|
||||
return
|
||||
ensure_resolveurl_installed(force=False, silent=True)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Update-Kanal anwenden / Sync
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
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_DEV:
|
||||
raw = _get_setting_string("update_repo_url_dev")
|
||||
elif channel == UPDATE_CHANNEL_CUSTOM:
|
||||
raw = _get_setting_string("update_repo_url")
|
||||
else:
|
||||
raw = _get_setting_string("update_repo_url_main")
|
||||
return normalize_update_info_url(raw)
|
||||
|
||||
|
||||
def sync_update_channel_status_settings() -> None:
|
||||
channel = selected_update_channel()
|
||||
selected_info_url = resolve_update_info_url()
|
||||
available_selected = fetch_repo_addon_version(selected_info_url)
|
||||
_set_setting_string("update_active_channel", channel_label(channel))
|
||||
_set_setting_string("update_active_repo_url", selected_info_url)
|
||||
_set_setting_string("update_available_selected", available_selected)
|
||||
|
||||
|
||||
def sync_update_version_settings() -> None:
|
||||
addon_version = installed_addon_version_from_disk()
|
||||
if addon_version == "0.0.0":
|
||||
addon = _get_addon()
|
||||
if addon is not None:
|
||||
try:
|
||||
addon_version = str(addon.getAddonInfo("version") or "0.0.0")
|
||||
except Exception:
|
||||
addon_version = "0.0.0"
|
||||
_set_setting_string("update_installed_version", addon_version)
|
||||
sync_resolveurl_status_setting()
|
||||
sync_update_channel_status_settings()
|
||||
|
||||
|
||||
def apply_update_channel(*, silent: bool = False) -> bool:
|
||||
if xbmc is None: # pragma: no cover - outside Kodi
|
||||
return False
|
||||
info_url = resolve_update_info_url()
|
||||
channel = selected_update_channel()
|
||||
sync_update_version_settings()
|
||||
applied = update_repository_source(info_url)
|
||||
installed_version = _get_setting_string("update_installed_version").strip() or "0.0.0"
|
||||
versions = filter_versions_for_channel(channel, fetch_repo_versions(info_url))
|
||||
target_version = versions[0] if versions else "-"
|
||||
|
||||
install_result = False
|
||||
if target_version != "-" and target_version != installed_version:
|
||||
install_result = install_addon_version(info_url, target_version)
|
||||
elif target_version == installed_version:
|
||||
install_result = True
|
||||
|
||||
builtin = getattr(xbmc, "executebuiltin", None)
|
||||
if callable(builtin):
|
||||
builtin("UpdateAddonRepos")
|
||||
builtin("UpdateLocalAddons")
|
||||
if not silent:
|
||||
if not applied:
|
||||
warning_icon = getattr(xbmcgui, "NOTIFICATION_WARNING", xbmcgui.NOTIFICATION_INFO)
|
||||
show_notification(
|
||||
"Updates",
|
||||
"Kanal gespeichert, aber repository.viewit nicht gefunden.",
|
||||
icon=warning_icon,
|
||||
milliseconds=5000,
|
||||
)
|
||||
elif target_version == "-":
|
||||
show_error("Updates", "Kanal angewendet, aber keine Version im Kanal gefunden.", milliseconds=5000)
|
||||
elif not install_result:
|
||||
show_error(
|
||||
"Updates",
|
||||
f"Kanal angewendet, Installation von {target_version} fehlgeschlagen.",
|
||||
milliseconds=5000,
|
||||
)
|
||||
elif target_version == installed_version:
|
||||
show_notification(
|
||||
"Updates",
|
||||
f"Kanal angewendet: {channel_label(selected_update_channel())} ({target_version} bereits installiert)",
|
||||
milliseconds=4500,
|
||||
)
|
||||
else:
|
||||
show_notification(
|
||||
"Updates",
|
||||
f"Kanal angewendet: {channel_label(selected_update_channel())} -> {target_version} installiert",
|
||||
milliseconds=5000,
|
||||
)
|
||||
sync_update_version_settings()
|
||||
return applied and install_result
|
||||
|
||||
|
||||
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:
|
||||
apply_update_channel(silent=True)
|
||||
if not silent:
|
||||
builtin = getattr(xbmc, "executebuiltin", None)
|
||||
if callable(builtin):
|
||||
builtin("ActivateWindow(addonbrowser,addons://updates/)")
|
||||
if not silent:
|
||||
show_notification("Updates", "Update-Check gestartet.", milliseconds=4000)
|
||||
except Exception as exc:
|
||||
_log(f"Update-Pruefung fehlgeschlagen: {exc}", 2)
|
||||
if not silent:
|
||||
show_error("Updates", "Update-Check fehlgeschlagen.", milliseconds=4000)
|
||||
|
||||
|
||||
def show_version_selector() -> None:
|
||||
if xbmc is None: # pragma: no cover - outside Kodi
|
||||
return
|
||||
|
||||
info_url = resolve_update_info_url()
|
||||
channel = selected_update_channel()
|
||||
sync_update_version_settings()
|
||||
|
||||
versions = filter_versions_for_channel(channel, fetch_repo_versions(info_url))
|
||||
if not versions:
|
||||
show_error("Updates", "Keine Versionen im Repo gefunden.", milliseconds=4000)
|
||||
return
|
||||
|
||||
installed = _get_setting_string("update_installed_version").strip() or "-"
|
||||
options = []
|
||||
for version in versions:
|
||||
label = version
|
||||
if version == installed:
|
||||
label = f"{version} (installiert)"
|
||||
options.append(label)
|
||||
|
||||
selected = xbmcgui.Dialog().select("Version waehlen", options)
|
||||
if selected < 0 or selected >= len(versions):
|
||||
return
|
||||
|
||||
version = versions[selected]
|
||||
changelog = fetch_changelog_for_channel(channel, version)
|
||||
viewer = getattr(xbmcgui.Dialog(), "textviewer", None)
|
||||
if callable(viewer):
|
||||
try:
|
||||
viewer(f"Changelog {version}", changelog)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
dialog = xbmcgui.Dialog()
|
||||
try:
|
||||
confirmed = dialog.yesno(
|
||||
"Version installieren",
|
||||
f"Installiert: {installed}",
|
||||
f"Ausgewaehlt: {version}",
|
||||
yeslabel="Installieren",
|
||||
nolabel="Abbrechen",
|
||||
)
|
||||
except TypeError:
|
||||
confirmed = dialog.yesno("Version installieren", f"Installiert: {installed}", f"Ausgewaehlt: {version}")
|
||||
if not confirmed:
|
||||
return
|
||||
|
||||
show_notification("Updates", f"Installation gestartet: {version}", milliseconds=2500)
|
||||
ok = install_addon_version(info_url, version)
|
||||
if ok:
|
||||
sync_update_version_settings()
|
||||
show_notification("Updates", f"Version {version} installiert.", milliseconds=4000)
|
||||
else:
|
||||
show_error("Updates", f"Installation von {version} fehlgeschlagen.", milliseconds=4500)
|
||||
|
||||
|
||||
def maybe_run_auto_update_check(action: str | None) -> None:
|
||||
action = (action or "").strip()
|
||||
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)
|
||||
Reference in New Issue
Block a user