From d876d5b84c7bb0f79b81a7a4c452e85bf4fddcb4 Mon Sep 17 00:00:00 2001 From: "itdrui.de" Date: Mon, 23 Feb 2026 20:44:33 +0100 Subject: [PATCH] updates: add version picker with changelog and install/cancel flow --- addon/default.py | 214 ++++++++++++++++++++++++++++++++++- addon/resources/settings.xml | 1 + 2 files changed, 214 insertions(+), 1 deletion(-) diff --git a/addon/default.py b/addon/default.py index ea45fce..1b27107 100644 --- a/addon/default.py +++ b/addon/default.py @@ -14,15 +14,17 @@ from datetime import datetime import importlib.util import inspect import json +import io import os import re import sys import threading import time import xml.etree.ElementTree as ET +import zipfile from pathlib import Path from types import ModuleType -from urllib.parse import parse_qs, urlencode +from urllib.parse import parse_qs, urlencode, urlparse from urllib.error import URLError from urllib.request import Request, urlopen @@ -991,6 +993,19 @@ def _channel_label(channel: int) -> str: return "Main" +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 _resolve_update_info_url() -> str: channel = _selected_update_channel() if channel == UPDATE_CHANNEL_NIGHTLY: @@ -1047,6 +1062,145 @@ def _fetch_repo_addon_version(info_url: str) -> str: return _extract_repo_addon_version(xml_text) +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 + + +def _extract_repo_identity(info_url: str) -> tuple[str, str, str, str] | None: + 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 + # expected: /{owner}/{repo}/raw/branch/{branch}/addons.xml + 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 + + +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: + if 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) + + +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" + try: + zip_bytes = _read_binary_url(zip_url) + except Exception as exc: + _log(f"Download fehlgeschlagen ({zip_url}): {exc}", xbmc.LOGWARNING) + 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): + continue + 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}", xbmc.LOGWARNING) + return False + + builtin = getattr(xbmc, "executebuiltin", None) + if callable(builtin): + builtin("UpdateLocalAddons") + return True + + def _sync_update_channel_status_settings() -> None: channel = _selected_update_channel() channel_label = _channel_label(channel) @@ -3256,6 +3410,62 @@ def _run_update_check(*, silent: bool = False) -> None: pass +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 = _fetch_repo_versions(info_url) + if not versions: + xbmcgui.Dialog().notification("Updates", "Keine Versionen im Repo gefunden.", xbmcgui.NOTIFICATION_ERROR, 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: + confirm = dialog.yesno( + "Version installieren", + f"Version: {version}", + "Installation jetzt starten?", + yeslabel="Installieren", + nolabel="Abbrechen", + ) + except TypeError: + confirm = dialog.yesno("Version installieren", f"Version: {version}", "Installation jetzt starten?") + if not confirm: + return + + ok = _install_addon_version(info_url, version) + if ok: + _sync_update_version_settings() + xbmcgui.Dialog().notification("Updates", f"Version {version} installiert.", xbmcgui.NOTIFICATION_INFO, 4000) + else: + xbmcgui.Dialog().notification("Updates", f"Installation von {version} fehlgeschlagen.", xbmcgui.NOTIFICATION_ERROR, 4500) + + def _maybe_run_auto_update_check(action: str | None) -> None: action = (action or "").strip() # Auto-Check nur beim Root-Menue, nicht in jedem Untermenue. @@ -3683,6 +3893,8 @@ def run() -> None: _run_update_check() elif action == "apply_update_channel": _apply_update_channel() + elif action == "select_update_version": + _show_version_selector() elif action == "seasons": _show_seasons(params.get("plugin", ""), params.get("title", ""), params.get("series_url", "")) elif action == "episodes": diff --git a/addon/resources/settings.xml b/addon/resources/settings.xml index 876fd6c..6a859ed 100644 --- a/addon/resources/settings.xml +++ b/addon/resources/settings.xml @@ -40,6 +40,7 @@ +