updates: add version picker with changelog and install/cancel flow
This commit is contained in:
214
addon/default.py
214
addon/default.py
@@ -14,15 +14,17 @@ from datetime import datetime
|
|||||||
import importlib.util
|
import importlib.util
|
||||||
import inspect
|
import inspect
|
||||||
import json
|
import json
|
||||||
|
import io
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
import time
|
import time
|
||||||
import xml.etree.ElementTree as ET
|
import xml.etree.ElementTree as ET
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from types import ModuleType
|
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.error import URLError
|
||||||
from urllib.request import Request, urlopen
|
from urllib.request import Request, urlopen
|
||||||
|
|
||||||
@@ -991,6 +993,19 @@ def _channel_label(channel: int) -> str:
|
|||||||
return "Main"
|
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:
|
def _resolve_update_info_url() -> str:
|
||||||
channel = _selected_update_channel()
|
channel = _selected_update_channel()
|
||||||
if channel == UPDATE_CHANNEL_NIGHTLY:
|
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)
|
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:
|
def _sync_update_channel_status_settings() -> None:
|
||||||
channel = _selected_update_channel()
|
channel = _selected_update_channel()
|
||||||
channel_label = _channel_label(channel)
|
channel_label = _channel_label(channel)
|
||||||
@@ -3256,6 +3410,62 @@ def _run_update_check(*, silent: bool = False) -> None:
|
|||||||
pass
|
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:
|
def _maybe_run_auto_update_check(action: str | None) -> None:
|
||||||
action = (action or "").strip()
|
action = (action or "").strip()
|
||||||
# Auto-Check nur beim Root-Menue, nicht in jedem Untermenue.
|
# Auto-Check nur beim Root-Menue, nicht in jedem Untermenue.
|
||||||
@@ -3683,6 +3893,8 @@ def run() -> None:
|
|||||||
_run_update_check()
|
_run_update_check()
|
||||||
elif action == "apply_update_channel":
|
elif action == "apply_update_channel":
|
||||||
_apply_update_channel()
|
_apply_update_channel()
|
||||||
|
elif action == "select_update_version":
|
||||||
|
_show_version_selector()
|
||||||
elif action == "seasons":
|
elif action == "seasons":
|
||||||
_show_seasons(params.get("plugin", ""), params.get("title", ""), params.get("series_url", ""))
|
_show_seasons(params.get("plugin", ""), params.get("title", ""), params.get("series_url", ""))
|
||||||
elif action == "episodes":
|
elif action == "episodes":
|
||||||
|
|||||||
@@ -40,6 +40,7 @@
|
|||||||
<setting id="apply_update_channel" type="action" label="Update-Kanal jetzt anwenden" action="RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel)" option="close" />
|
<setting id="apply_update_channel" type="action" label="Update-Kanal jetzt anwenden" action="RunPlugin(plugin://plugin.video.viewit/?action=apply_update_channel)" option="close" />
|
||||||
<setting id="auto_update_enabled" type="bool" label="Automatische Updates (beim Start pruefen)" default="false" />
|
<setting id="auto_update_enabled" type="bool" label="Automatische Updates (beim Start pruefen)" default="false" />
|
||||||
<setting id="run_update_check" type="action" label="Jetzt nach Updates suchen" action="RunPlugin(plugin://plugin.video.viewit/?action=check_updates)" option="close" />
|
<setting id="run_update_check" type="action" label="Jetzt nach Updates suchen" action="RunPlugin(plugin://plugin.video.viewit/?action=check_updates)" option="close" />
|
||||||
|
<setting id="select_update_version" type="action" label="Version waehlen und installieren" action="RunPlugin(plugin://plugin.video.viewit/?action=select_update_version)" option="close" />
|
||||||
<setting id="update_installed_version" type="text" label="Installierte Version" default="-" enable="false" />
|
<setting id="update_installed_version" type="text" label="Installierte Version" default="-" enable="false" />
|
||||||
<setting id="update_available_selected" type="text" label="Verfuegbar (gewaehlter Kanal)" default="-" enable="false" />
|
<setting id="update_available_selected" type="text" label="Verfuegbar (gewaehlter Kanal)" default="-" enable="false" />
|
||||||
<setting id="update_available_main" type="text" label="Verfuegbar Main" default="-" enable="false" />
|
<setting id="update_available_main" type="text" label="Verfuegbar Main" default="-" enable="false" />
|
||||||
|
|||||||
Reference in New Issue
Block a user