release: 0.1.61
This commit is contained in:
29
CHANGELOG-NIGHTLY.md
Normal file
29
CHANGELOG-NIGHTLY.md
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
# Changelog (Nightly)
|
||||||
|
|
||||||
|
## 0.1.61-nightly - 2026-02-23
|
||||||
|
|
||||||
|
- Update-Dialog: feste Auswahl mit `Installieren` / `Abbrechen` (kein vertauschter Yes/No-Dialog mehr).
|
||||||
|
- Versionen im Update-Dialog nach Kanal gefiltert:
|
||||||
|
- Main: nur `x.y.z`
|
||||||
|
- Nightly: nur `x.y.z-nightly`
|
||||||
|
- Installierte Version wird direkt aus `addon.xml` gelesen.
|
||||||
|
- Beim Kanalwechsel wird direkt die neueste Version aus dem gewaehlten Kanal installiert.
|
||||||
|
|
||||||
|
## 0.1.59-nightly - 2026-02-23
|
||||||
|
|
||||||
|
- Enthaelt alle Aenderungen aus `0.1.58`.
|
||||||
|
- Update-Kanal standardmaessig auf `Nightly`.
|
||||||
|
- Nightly-Repo-URL als Standard gesetzt.
|
||||||
|
- Settings-Menue neu sortiert:
|
||||||
|
- Quellen
|
||||||
|
- Metadaten
|
||||||
|
- TMDB Erweitert
|
||||||
|
- Updates
|
||||||
|
- Debug Global
|
||||||
|
- Debug Quellen
|
||||||
|
- Seitengroesse in Listen auf 20 gesetzt.
|
||||||
|
- `topstream_genre_max_pages` entfernt.
|
||||||
|
|
||||||
|
## Hinweis
|
||||||
|
|
||||||
|
- Nightly ist fuer Tests und kann sich kurzfristig aendern.
|
||||||
23
CHANGELOG.md
Normal file
23
CHANGELOG.md
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
# Changelog (Stable)
|
||||||
|
|
||||||
|
## 0.1.61 - 2026-02-23
|
||||||
|
|
||||||
|
- Menues und Labels weiter vereinheitlicht (ASCII-only, einheitliche Texte pro Plugin).
|
||||||
|
- Update-Bereich ueberarbeitet:
|
||||||
|
- Kanalwechsel mit direkter Installation der neuesten Kanal-Version.
|
||||||
|
- Version-Auswahl mit Changelog-Anzeige und klarer Installieren/Abbrechen-Auswahl.
|
||||||
|
- Anzeige der installierten Version direkt aus lokaler `addon.xml`.
|
||||||
|
- Kanal-spezifischer Versionsfilter (Main nur stable, Nightly nur `-nightly`).
|
||||||
|
- Resolver-/Playback-Flow vereinheitlicht und Hoster-URL-Normalisierung zentralisiert.
|
||||||
|
- Settings aufgeraeumt (strukturierte Kategorien, reduzierte Alt-Optionen).
|
||||||
|
|
||||||
|
## 0.1.58 - 2026-02-23
|
||||||
|
|
||||||
|
- Menuebezeichnungen vereinheitlicht (`Haeufig gesehen`, `Neuste Titel`).
|
||||||
|
- `Neue Titel` und `Neueste Folgen` im Menue zu `Neuste Titel` zusammengelegt.
|
||||||
|
- Hoster-Header-Anpassung zentral nach `resolve_stream_link` eingebaut.
|
||||||
|
- Hinweis bei Cloudflare-Block durch ResolveURL statt stiller Fehlversuche.
|
||||||
|
- Update-Einstellungen erweitert (Kanal, manueller Check, optionaler Auto-Check).
|
||||||
|
- Metadaten-Parsing in AniWorld und Filmpalast nachgezogen (Cover/Plot robuster).
|
||||||
|
- Topstreamfilm-Suche: fehlender `urlencode`-Import behoben.
|
||||||
|
- Einige ungenutzte Funktionen entfernt.
|
||||||
14
README.md
14
README.md
@@ -29,20 +29,6 @@ Es durchsucht Provider und startet Streams.
|
|||||||
- Plugins: `addon/plugins/*_plugin.py`
|
- Plugins: `addon/plugins/*_plugin.py`
|
||||||
- Settings: `addon/resources/settings.xml`
|
- Settings: `addon/resources/settings.xml`
|
||||||
|
|
||||||
## TMDB API Key einrichten
|
|
||||||
- TMDB Account anlegen und API Key (v3) erstellen: `https://www.themoviedb.org/settings/api`
|
|
||||||
- In Kodi das ViewIT Addon oeffnen: `Einstellungen -> TMDB`
|
|
||||||
- `TMDB aktivieren` einschalten
|
|
||||||
- `TMDB API Key` eintragen
|
|
||||||
- Optional `TMDB Sprache` setzen (z. B. `de-DE`)
|
|
||||||
- Optional die Anzeige-Optionen aktivieren/deaktivieren:
|
|
||||||
- `TMDB Beschreibung anzeigen`
|
|
||||||
- `TMDB Poster und Vorschaubild anzeigen`
|
|
||||||
- `TMDB Fanart/Backdrop anzeigen`
|
|
||||||
- `TMDB Bewertung anzeigen`
|
|
||||||
- `TMDB Stimmen anzeigen`
|
|
||||||
- `TMDB Besetzung anzeigen`
|
|
||||||
|
|
||||||
## Tests
|
## Tests
|
||||||
- Dev Pakete installieren: `./.venv/bin/pip install -r requirements-dev.txt`
|
- Dev Pakete installieren: `./.venv/bin/pip install -r requirements-dev.txt`
|
||||||
- Tests starten: `./.venv/bin/pytest`
|
- Tests starten: `./.venv/bin/pytest`
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?xml version='1.0' encoding='utf-8'?>
|
||||||
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.58" provider-name="ViewIt">
|
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.61" provider-name="ViewIt">
|
||||||
<requires>
|
<requires>
|
||||||
<import addon="xbmc.python" version="3.0.0" />
|
<import addon="xbmc.python" version="3.0.0" />
|
||||||
<import addon="script.module.requests" />
|
<import addon="script.module.requests" />
|
||||||
|
|||||||
533
addon/default.py
533
addon/default.py
@@ -14,15 +14,19 @@ 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.request import Request, urlopen
|
||||||
|
|
||||||
|
|
||||||
def _ensure_windows_selector_policy() -> None:
|
def _ensure_windows_selector_policy() -> None:
|
||||||
@@ -131,6 +135,7 @@ _TMDB_LOCK = threading.RLock()
|
|||||||
WATCHED_THRESHOLD = 0.9
|
WATCHED_THRESHOLD = 0.9
|
||||||
POPULAR_MENU_LABEL = "Haeufig gesehen"
|
POPULAR_MENU_LABEL = "Haeufig gesehen"
|
||||||
LATEST_MENU_LABEL = "Neuste Titel"
|
LATEST_MENU_LABEL = "Neuste Titel"
|
||||||
|
LIST_PAGE_SIZE = 20
|
||||||
|
|
||||||
atexit.register(close_all_sessions)
|
atexit.register(close_all_sessions)
|
||||||
|
|
||||||
@@ -305,10 +310,6 @@ def _playstate_key(*, plugin_name: str, title: str, season: str, episode: str) -
|
|||||||
return f"{plugin_name}\t{title}\t{season}\t{episode}"
|
return f"{plugin_name}\t{title}\t{season}\t{episode}"
|
||||||
|
|
||||||
|
|
||||||
def _playstate_path() -> str:
|
|
||||||
return _get_log_path("playstate.json")
|
|
||||||
|
|
||||||
|
|
||||||
def _load_playstate() -> dict[str, dict[str, object]]:
|
def _load_playstate() -> dict[str, dict[str, object]]:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -973,6 +974,8 @@ UPDATE_CHANNEL_MAIN = 0
|
|||||||
UPDATE_CHANNEL_NIGHTLY = 1
|
UPDATE_CHANNEL_NIGHTLY = 1
|
||||||
UPDATE_CHANNEL_CUSTOM = 2
|
UPDATE_CHANNEL_CUSTOM = 2
|
||||||
AUTO_UPDATE_INTERVAL_SEC = 6 * 60 * 60
|
AUTO_UPDATE_INTERVAL_SEC = 6 * 60 * 60
|
||||||
|
UPDATE_HTTP_TIMEOUT_SEC = 8
|
||||||
|
UPDATE_ADDON_ID = "plugin.video.viewit"
|
||||||
|
|
||||||
|
|
||||||
def _selected_update_channel() -> int:
|
def _selected_update_channel() -> int:
|
||||||
@@ -982,6 +985,43 @@ def _selected_update_channel() -> int:
|
|||||||
return channel
|
return channel
|
||||||
|
|
||||||
|
|
||||||
|
def _channel_label(channel: int) -> str:
|
||||||
|
if channel == UPDATE_CHANNEL_NIGHTLY:
|
||||||
|
return "Nightly"
|
||||||
|
if channel == UPDATE_CHANNEL_CUSTOM:
|
||||||
|
return "Custom"
|
||||||
|
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 _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 _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)]
|
||||||
|
return list(versions)
|
||||||
|
|
||||||
|
|
||||||
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:
|
||||||
@@ -996,6 +1036,234 @@ def _resolve_update_info_url() -> str:
|
|||||||
return info_url
|
return info_url
|
||||||
|
|
||||||
|
|
||||||
|
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 _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 _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_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}", 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 _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"
|
||||||
|
|
||||||
|
# Prefer Kodi's own installer to get proper install flow and dependency handling.
|
||||||
|
builtin = getattr(xbmc, "executebuiltin", 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}", xbmc.LOGWARNING)
|
||||||
|
|
||||||
|
return _install_addon_version_manual(info_url, version)
|
||||||
|
|
||||||
|
|
||||||
|
def _sync_update_channel_status_settings() -> None:
|
||||||
|
channel = _selected_update_channel()
|
||||||
|
channel_label = _channel_label(channel)
|
||||||
|
|
||||||
|
selected_info_url = _resolve_update_info_url()
|
||||||
|
main_info_url = _normalize_update_info_url(_get_setting_string("update_repo_url_main"))
|
||||||
|
nightly_info_url = _normalize_update_info_url(_get_setting_string("update_repo_url_nightly"))
|
||||||
|
|
||||||
|
available_main = _fetch_repo_addon_version(main_info_url)
|
||||||
|
available_nightly = _fetch_repo_addon_version(nightly_info_url)
|
||||||
|
if channel == UPDATE_CHANNEL_MAIN:
|
||||||
|
available_selected = available_main
|
||||||
|
elif channel == UPDATE_CHANNEL_NIGHTLY:
|
||||||
|
available_selected = available_nightly
|
||||||
|
else:
|
||||||
|
available_selected = _fetch_repo_addon_version(selected_info_url)
|
||||||
|
|
||||||
|
_set_setting_string("update_active_channel", channel_label)
|
||||||
|
_set_setting_string("update_active_repo_url", selected_info_url)
|
||||||
|
_set_setting_string("update_available_main", available_main)
|
||||||
|
_set_setting_string("update_available_nightly", available_nightly)
|
||||||
|
_set_setting_string("update_available_selected", available_selected)
|
||||||
|
|
||||||
|
|
||||||
def _repo_addon_xml_path() -> str:
|
def _repo_addon_xml_path() -> str:
|
||||||
if xbmcvfs is None:
|
if xbmcvfs is None:
|
||||||
return ""
|
return ""
|
||||||
@@ -1038,15 +1306,34 @@ def _settings_key_for_plugin(name: str) -> str:
|
|||||||
return f"update_version_{safe}" if safe else "update_version_unknown"
|
return f"update_version_{safe}" if safe else "update_version_unknown"
|
||||||
|
|
||||||
|
|
||||||
|
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 _sync_update_version_settings() -> None:
|
def _sync_update_version_settings() -> None:
|
||||||
addon = _get_addon()
|
addon_version = _installed_addon_version_from_disk()
|
||||||
addon_version = "0.0.0"
|
if addon_version == "0.0.0":
|
||||||
if addon is not None:
|
addon = _get_addon()
|
||||||
try:
|
if addon is not None:
|
||||||
addon_version = str(addon.getAddonInfo("version") or "0.0.0")
|
try:
|
||||||
except Exception:
|
addon_version = str(addon.getAddonInfo("version") or "0.0.0")
|
||||||
addon_version = "0.0.0"
|
except Exception:
|
||||||
|
addon_version = "0.0.0"
|
||||||
_set_setting_string("update_version_addon", addon_version)
|
_set_setting_string("update_version_addon", addon_version)
|
||||||
|
_set_setting_string("update_installed_version", addon_version)
|
||||||
|
|
||||||
versions = {
|
versions = {
|
||||||
"update_version_serienstream": "-",
|
"update_version_serienstream": "-",
|
||||||
@@ -1063,6 +1350,8 @@ def _sync_update_version_settings() -> None:
|
|||||||
for key, value in versions.items():
|
for key, value in versions.items():
|
||||||
_set_setting_string(key, value)
|
_set_setting_string(key, value)
|
||||||
|
|
||||||
|
_sync_update_channel_status_settings()
|
||||||
|
|
||||||
|
|
||||||
def _show_root_menu() -> None:
|
def _show_root_menu() -> None:
|
||||||
handle = _get_handle()
|
handle = _get_handle()
|
||||||
@@ -2574,7 +2863,7 @@ def _plugins_with_popular() -> list[tuple[str, BasisPlugin, str]]:
|
|||||||
|
|
||||||
def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
||||||
handle = _get_handle()
|
handle = _get_handle()
|
||||||
page_size = 10
|
page_size = LIST_PAGE_SIZE
|
||||||
page = max(1, int(page or 1))
|
page = max(1, int(page or 1))
|
||||||
|
|
||||||
if plugin_name:
|
if plugin_name:
|
||||||
@@ -2701,7 +2990,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
|
|||||||
|
|
||||||
def _show_new_titles(plugin_name: str, page: int = 1, *, action_name: str = "new_titles") -> None:
|
def _show_new_titles(plugin_name: str, page: int = 1, *, action_name: str = "new_titles") -> None:
|
||||||
handle = _get_handle()
|
handle = _get_handle()
|
||||||
page_size = 10
|
page_size = LIST_PAGE_SIZE
|
||||||
page = max(1, int(page or 1))
|
page = max(1, int(page or 1))
|
||||||
|
|
||||||
plugin_name = (plugin_name or "").strip()
|
plugin_name = (plugin_name or "").strip()
|
||||||
@@ -2949,7 +3238,7 @@ def _show_latest_titles(plugin_name: str, page: int = 1) -> None:
|
|||||||
|
|
||||||
def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page: int = 1) -> None:
|
def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page: int = 1) -> None:
|
||||||
handle = _get_handle()
|
handle = _get_handle()
|
||||||
page_size = 10
|
page_size = LIST_PAGE_SIZE
|
||||||
page = max(1, int(page or 1))
|
page = max(1, int(page or 1))
|
||||||
plugin = _discover_plugins().get(plugin_name)
|
plugin = _discover_plugins().get(plugin_name)
|
||||||
if plugin is None:
|
if plugin is None:
|
||||||
@@ -3132,19 +3421,77 @@ def _open_settings() -> None:
|
|||||||
addon.openSettings()
|
addon.openSettings()
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Updates",
|
||||||
|
"Kanal gespeichert, aber repository.viewit nicht gefunden.",
|
||||||
|
warning_icon,
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
elif target_version == "-":
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Updates",
|
||||||
|
"Kanal angewendet, aber keine Version im Kanal gefunden.",
|
||||||
|
xbmcgui.NOTIFICATION_ERROR,
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
elif not install_result:
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Updates",
|
||||||
|
f"Kanal angewendet, Installation von {target_version} fehlgeschlagen.",
|
||||||
|
xbmcgui.NOTIFICATION_ERROR,
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
elif target_version == installed_version:
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Updates",
|
||||||
|
f"Kanal angewendet: {_channel_label(_selected_update_channel())} ({target_version} bereits installiert)",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
4500,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Updates",
|
||||||
|
f"Kanal angewendet: {_channel_label(_selected_update_channel())} -> {target_version} installiert",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
5000,
|
||||||
|
)
|
||||||
|
_sync_update_version_settings()
|
||||||
|
return applied and install_result
|
||||||
|
|
||||||
|
|
||||||
def _run_update_check(*, silent: bool = False) -> None:
|
def _run_update_check(*, silent: bool = False) -> None:
|
||||||
"""Stoesst Kodi-Repo- und Addon-Updates an."""
|
"""Stoesst Kodi-Repo- und Addon-Updates an."""
|
||||||
if xbmc is None: # pragma: no cover - outside Kodi
|
if xbmc is None: # pragma: no cover - outside Kodi
|
||||||
return
|
return
|
||||||
try:
|
try:
|
||||||
info_url = _resolve_update_info_url()
|
_apply_update_channel(silent=True)
|
||||||
_sync_update_version_settings()
|
if not silent:
|
||||||
_update_repository_source(info_url)
|
builtin = getattr(xbmc, "executebuiltin", None)
|
||||||
builtin = getattr(xbmc, "executebuiltin", None)
|
if callable(builtin):
|
||||||
if callable(builtin):
|
|
||||||
builtin("UpdateAddonRepos")
|
|
||||||
builtin("UpdateLocalAddons")
|
|
||||||
if not silent:
|
|
||||||
builtin("ActivateWindow(addonbrowser,addons://updates/)")
|
builtin("ActivateWindow(addonbrowser,addons://updates/)")
|
||||||
if not silent:
|
if not silent:
|
||||||
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000)
|
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000)
|
||||||
@@ -3157,6 +3504,59 @@ 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 = _filter_versions_for_channel(channel, _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
|
||||||
|
|
||||||
|
confirm_choice = xbmcgui.Dialog().select(
|
||||||
|
"Version installieren",
|
||||||
|
[
|
||||||
|
f"Installieren: {version}",
|
||||||
|
"Abbrechen",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
if confirm_choice != 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
xbmcgui.Dialog().notification("Updates", f"Installation gestartet: {version}", xbmcgui.NOTIFICATION_INFO, 2500)
|
||||||
|
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.
|
||||||
@@ -3182,23 +3582,28 @@ def _extract_first_int(value: str) -> int | None:
|
|||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
def _duration_label(duration_seconds: int) -> str:
|
|
||||||
try:
|
|
||||||
duration_seconds = int(duration_seconds or 0)
|
|
||||||
except Exception:
|
|
||||||
duration_seconds = 0
|
|
||||||
if duration_seconds <= 0:
|
|
||||||
return ""
|
|
||||||
total_minutes = max(0, duration_seconds // 60)
|
|
||||||
hours = max(0, total_minutes // 60)
|
|
||||||
minutes = max(0, total_minutes % 60)
|
|
||||||
return f"{hours:02d}:{minutes:02d} Laufzeit"
|
|
||||||
|
|
||||||
|
|
||||||
def _label_with_duration(label: str, info_labels: dict[str, str] | None) -> str:
|
def _label_with_duration(label: str, info_labels: dict[str, str] | None) -> str:
|
||||||
return label
|
return label
|
||||||
|
|
||||||
|
|
||||||
|
def _resolveurl_last_error() -> str:
|
||||||
|
try:
|
||||||
|
from resolveurl_backend import get_last_error # type: ignore
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
try:
|
||||||
|
return str(get_last_error() or "")
|
||||||
|
except Exception:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def _is_cloudflare_challenge_error(message: str) -> bool:
|
||||||
|
text = str(message or "").casefold()
|
||||||
|
if not text:
|
||||||
|
return False
|
||||||
|
return "cloudflare" in text or "challenge" in text or "attention required" in text
|
||||||
|
|
||||||
|
|
||||||
def _play_final_link(
|
def _play_final_link(
|
||||||
link: str,
|
link: str,
|
||||||
*,
|
*,
|
||||||
@@ -3242,10 +3647,6 @@ def _play_final_link(
|
|||||||
player.play(item=link, listitem=list_item)
|
player.play(item=link, listitem=list_item)
|
||||||
|
|
||||||
|
|
||||||
def _track_playback_and_update_state(key: str) -> None:
|
|
||||||
return
|
|
||||||
|
|
||||||
|
|
||||||
def _track_playback_and_update_state_async(key: str) -> None:
|
def _track_playback_and_update_state_async(key: str) -> None:
|
||||||
# Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst.
|
# Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst.
|
||||||
return
|
return
|
||||||
@@ -3334,8 +3735,30 @@ def _play_episode(
|
|||||||
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||||
return
|
return
|
||||||
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
||||||
final_link = plugin.resolve_stream_link(link) or link
|
resolved_link = plugin.resolve_stream_link(link)
|
||||||
|
if not resolved_link:
|
||||||
|
err = _resolveurl_last_error()
|
||||||
|
if _is_cloudflare_challenge_error(err):
|
||||||
|
_log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Wiedergabe",
|
||||||
|
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
4500,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
final_link = resolved_link or link
|
||||||
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
||||||
|
err = _resolveurl_last_error()
|
||||||
|
if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip():
|
||||||
|
_log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Wiedergabe",
|
||||||
|
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
4500,
|
||||||
|
)
|
||||||
|
return
|
||||||
finally:
|
finally:
|
||||||
if restore_hosters is not None and callable(preferred_setter):
|
if restore_hosters is not None and callable(preferred_setter):
|
||||||
preferred_setter(restore_hosters)
|
preferred_setter(restore_hosters)
|
||||||
@@ -3422,8 +3845,30 @@ def _play_episode_url(
|
|||||||
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||||
return
|
return
|
||||||
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
|
||||||
final_link = plugin.resolve_stream_link(link) or link
|
resolved_link = plugin.resolve_stream_link(link)
|
||||||
|
if not resolved_link:
|
||||||
|
err = _resolveurl_last_error()
|
||||||
|
if _is_cloudflare_challenge_error(err):
|
||||||
|
_log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Wiedergabe",
|
||||||
|
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
4500,
|
||||||
|
)
|
||||||
|
return
|
||||||
|
final_link = resolved_link or link
|
||||||
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
final_link = normalize_resolved_stream_url(final_link, source_url=link)
|
||||||
|
err = _resolveurl_last_error()
|
||||||
|
if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip():
|
||||||
|
_log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING)
|
||||||
|
xbmcgui.Dialog().notification(
|
||||||
|
"Wiedergabe",
|
||||||
|
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
|
||||||
|
xbmcgui.NOTIFICATION_INFO,
|
||||||
|
4500,
|
||||||
|
)
|
||||||
|
return
|
||||||
finally:
|
finally:
|
||||||
if restore_hosters is not None and callable(preferred_setter):
|
if restore_hosters is not None and callable(preferred_setter):
|
||||||
preferred_setter(restore_hosters)
|
preferred_setter(restore_hosters)
|
||||||
@@ -3537,6 +3982,10 @@ def run() -> None:
|
|||||||
_open_settings()
|
_open_settings()
|
||||||
elif action == "check_updates":
|
elif action == "check_updates":
|
||||||
_run_update_check()
|
_run_update_check()
|
||||||
|
elif action == "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":
|
||||||
|
|||||||
@@ -603,15 +603,6 @@ class EinschaltenPlugin(BasisPlugin):
|
|||||||
url = urljoin(base + "/", path.lstrip("/"))
|
url = urljoin(base + "/", path.lstrip("/"))
|
||||||
return f"{url}?{urlencode({'query': query})}"
|
return f"{url}?{urlencode({'query': query})}"
|
||||||
|
|
||||||
def _api_movies_url(self, *, with_genres: int, page: int = 1) -> str:
|
|
||||||
base = self._get_base_url()
|
|
||||||
if not base:
|
|
||||||
return ""
|
|
||||||
params: Dict[str, str] = {"withGenres": str(int(with_genres))}
|
|
||||||
if page and int(page) > 1:
|
|
||||||
params["page"] = str(int(page))
|
|
||||||
return urljoin(base + "/", "api/movies") + f"?{urlencode(params)}"
|
|
||||||
|
|
||||||
def _genre_page_url(self, *, genre_id: int, page: int = 1) -> str:
|
def _genre_page_url(self, *, genre_id: int, page: int = 1) -> str:
|
||||||
"""Genre title pages are rendered server-side and embed the movie list in ng-state.
|
"""Genre title pages are rendered server-side and embed the movie list in ng-state.
|
||||||
|
|
||||||
@@ -771,23 +762,6 @@ class EinschaltenPlugin(BasisPlugin):
|
|||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _fetch_new_titles_movies(self) -> List[MovieItem]:
|
|
||||||
# "Neue Filme" lives at `/movies/new` and embeds the list in ng-state (`u: "/api/movies"`).
|
|
||||||
url = self._new_titles_url()
|
|
||||||
if not url:
|
|
||||||
return []
|
|
||||||
try:
|
|
||||||
_, body = self._http_get_text(url, timeout=20)
|
|
||||||
payload = _extract_ng_state_payload(body)
|
|
||||||
movies = _parse_ng_state_movies(payload)
|
|
||||||
_log_debug_line(f"parse_ng_state_movies:count={len(movies)}")
|
|
||||||
if movies:
|
|
||||||
_log_titles(movies, context="new_titles")
|
|
||||||
return movies
|
|
||||||
return []
|
|
||||||
except Exception:
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _fetch_new_titles_movies_page(self, page: int) -> List[MovieItem]:
|
def _fetch_new_titles_movies_page(self, page: int) -> List[MovieItem]:
|
||||||
page = max(1, int(page or 1))
|
page = max(1, int(page or 1))
|
||||||
url = self._new_titles_url()
|
url = self._new_titles_url()
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import os
|
|||||||
import re
|
import re
|
||||||
import json
|
import json
|
||||||
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urlencode, urljoin
|
||||||
|
|
||||||
try: # pragma: no cover - optional dependency
|
try: # pragma: no cover - optional dependency
|
||||||
import requests
|
import requests
|
||||||
@@ -66,12 +66,9 @@ SETTING_LOG_URLS = "log_urls_topstreamfilm"
|
|||||||
SETTING_DUMP_HTML = "dump_html_topstreamfilm"
|
SETTING_DUMP_HTML = "dump_html_topstreamfilm"
|
||||||
SETTING_SHOW_URL_INFO = "show_url_info_topstreamfilm"
|
SETTING_SHOW_URL_INFO = "show_url_info_topstreamfilm"
|
||||||
SETTING_LOG_ERRORS = "log_errors_topstreamfilm"
|
SETTING_LOG_ERRORS = "log_errors_topstreamfilm"
|
||||||
SETTING_GENRE_MAX_PAGES = "topstream_genre_max_pages"
|
|
||||||
DEFAULT_TIMEOUT = 20
|
DEFAULT_TIMEOUT = 20
|
||||||
DEFAULT_PREFERRED_HOSTERS = ["supervideo", "dropload", "voe"]
|
DEFAULT_PREFERRED_HOSTERS = ["supervideo", "dropload", "voe"]
|
||||||
MEINECLOUD_HOST = "meinecloud.click"
|
MEINECLOUD_HOST = "meinecloud.click"
|
||||||
DEFAULT_GENRE_MAX_PAGES = 20
|
|
||||||
HARD_MAX_GENRE_PAGES = 200
|
|
||||||
HEADERS = {
|
HEADERS = {
|
||||||
"User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)",
|
"User-Agent": "Mozilla/5.0 (Kodi; ViewIt) AppleWebKit/537.36 (KHTML, like Gecko)",
|
||||||
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
|
||||||
@@ -347,22 +344,6 @@ class TopstreamfilmPlugin(BasisPlugin):
|
|||||||
return urljoin(base if base.endswith("/") else base + "/", href)
|
return urljoin(base if base.endswith("/") else base + "/", href)
|
||||||
return href
|
return href
|
||||||
|
|
||||||
def _get_setting_bool(self, setting_id: str, *, default: bool = False) -> bool:
|
|
||||||
return get_setting_bool(ADDON_ID, setting_id, default=default)
|
|
||||||
|
|
||||||
def _get_setting_int(self, setting_id: str, *, default: int) -> int:
|
|
||||||
if xbmcaddon is None:
|
|
||||||
return default
|
|
||||||
try:
|
|
||||||
addon = xbmcaddon.Addon(ADDON_ID)
|
|
||||||
getter = getattr(addon, "getSettingInt", None)
|
|
||||||
if callable(getter):
|
|
||||||
return int(getter(setting_id))
|
|
||||||
raw = str(addon.getSetting(setting_id) or "").strip()
|
|
||||||
return int(raw) if raw else default
|
|
||||||
except Exception:
|
|
||||||
return default
|
|
||||||
|
|
||||||
def _notify_url(self, url: str) -> None:
|
def _notify_url(self, url: str) -> None:
|
||||||
notify_url(
|
notify_url(
|
||||||
ADDON_ID,
|
ADDON_ID,
|
||||||
|
|||||||
@@ -8,8 +8,16 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
_LAST_RESOLVE_ERROR = ""
|
||||||
|
|
||||||
|
|
||||||
|
def get_last_error() -> str:
|
||||||
|
return str(_LAST_RESOLVE_ERROR or "")
|
||||||
|
|
||||||
|
|
||||||
def resolve(url: str) -> Optional[str]:
|
def resolve(url: str) -> Optional[str]:
|
||||||
|
global _LAST_RESOLVE_ERROR
|
||||||
|
_LAST_RESOLVE_ERROR = ""
|
||||||
if not url:
|
if not url:
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
@@ -23,12 +31,14 @@ def resolve(url: str) -> Optional[str]:
|
|||||||
hmf = hosted(url)
|
hmf = hosted(url)
|
||||||
valid = getattr(hmf, "valid_url", None)
|
valid = getattr(hmf, "valid_url", None)
|
||||||
if callable(valid) and not valid():
|
if callable(valid) and not valid():
|
||||||
|
_LAST_RESOLVE_ERROR = "invalid url"
|
||||||
return None
|
return None
|
||||||
resolver = getattr(hmf, "resolve", None)
|
resolver = getattr(hmf, "resolve", None)
|
||||||
if callable(resolver):
|
if callable(resolver):
|
||||||
result = resolver()
|
result = resolver()
|
||||||
return str(result) if result else None
|
return str(result) if result else None
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
_LAST_RESOLVE_ERROR = str(exc or "")
|
||||||
pass
|
pass
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -36,8 +46,8 @@ def resolve(url: str) -> Optional[str]:
|
|||||||
if callable(resolve_fn):
|
if callable(resolve_fn):
|
||||||
result = resolve_fn(url)
|
result = resolve_fn(url)
|
||||||
return str(result) if result else None
|
return str(result) if result else None
|
||||||
except Exception:
|
except Exception as exc:
|
||||||
|
_LAST_RESOLVE_ERROR = str(exc or "")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,66 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<settings>
|
<settings>
|
||||||
<category label="Debug und Logs">
|
<category label="Quellen">
|
||||||
|
<setting id="serienstream_base_url" type="text" label="SerienStream Basis-URL" default="https://s.to" />
|
||||||
|
<setting id="aniworld_base_url" type="text" label="AniWorld Basis-URL" default="https://aniworld.to" />
|
||||||
|
<setting id="topstream_base_url" type="text" label="TopStream Basis-URL" default="https://topstreamfilm.live" />
|
||||||
|
<setting id="einschalten_base_url" type="text" label="Einschalten Basis-URL" default="https://einschalten.in" />
|
||||||
|
<setting id="filmpalast_base_url" type="text" label="Filmpalast Basis-URL" default="https://filmpalast.to" />
|
||||||
|
<setting id="doku_streams_base_url" type="text" label="Doku-Streams Basis-URL" default="https://doku-streams.com" />
|
||||||
|
</category>
|
||||||
|
|
||||||
|
<category label="Metadaten">
|
||||||
|
<setting id="serienstream_metadata_source" type="enum" label="SerienStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="aniworld_metadata_source" type="enum" label="AniWorld Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="topstreamfilm_metadata_source" type="enum" label="TopStream Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="einschalten_metadata_source" type="enum" label="Einschalten Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="filmpalast_metadata_source" type="enum" label="Filmpalast Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="doku_streams_metadata_source" type="enum" label="Doku-Streams Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
||||||
|
<setting id="tmdb_enabled" type="bool" label="TMDB aktivieren" default="true" />
|
||||||
|
<setting id="tmdb_language" type="text" label="TMDB Sprache (z. B. de-DE)" default="de-DE" />
|
||||||
|
<setting id="tmdb_show_plot" type="bool" label="TMDB Beschreibung anzeigen" default="true" />
|
||||||
|
<setting id="tmdb_show_art" type="bool" label="TMDB Poster und Vorschaubild anzeigen" default="true" />
|
||||||
|
<setting id="tmdb_show_fanart" type="bool" label="TMDB Fanart/Backdrop anzeigen" default="true" />
|
||||||
|
<setting id="tmdb_show_rating" type="bool" label="TMDB Bewertung anzeigen" default="true" />
|
||||||
|
<setting id="tmdb_show_votes" type="bool" label="TMDB Stimmen anzeigen" default="false" />
|
||||||
|
</category>
|
||||||
|
|
||||||
|
<category label="TMDB Erweitert">
|
||||||
|
<setting id="tmdb_api_key" type="text" label="TMDB API Key" default="" />
|
||||||
|
<setting id="tmdb_prefetch_concurrency" type="number" label="TMDB: gleichzeitige Anfragen (1-20)" default="6" />
|
||||||
|
<setting id="tmdb_show_cast" type="bool" label="TMDB Besetzung anzeigen" default="false" />
|
||||||
|
<setting id="tmdb_show_episode_cast" type="bool" label="TMDB Besetzung pro Episode anzeigen" default="false" />
|
||||||
|
<setting id="tmdb_genre_metadata" type="bool" label="TMDB Daten in Genre-Listen anzeigen" default="false" />
|
||||||
|
<setting id="tmdb_log_requests" type="bool" label="TMDB API-Anfragen loggen" default="false" />
|
||||||
|
<setting id="tmdb_log_responses" type="bool" label="TMDB API-Antworten loggen" default="false" />
|
||||||
|
</category>
|
||||||
|
|
||||||
|
<category label="Updates">
|
||||||
|
<setting id="update_channel" type="enum" label="Update-Kanal" default="0" values="Main|Nightly|Custom" />
|
||||||
|
<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="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_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_nightly" type="text" label="Verfuegbar Nightly" default="-" enable="false" />
|
||||||
|
<setting id="update_active_channel" type="text" label="Aktiver Kanal" default="-" enable="false" />
|
||||||
|
<setting id="update_active_repo_url" type="text" label="Aktive Repo URL" default="-" enable="false" />
|
||||||
|
<setting id="update_info" type="text" label="Updates laufen ueber den normalen Kodi-Update-Mechanismus." default="" enable="false" />
|
||||||
|
<setting id="update_repo_url_main" type="text" label="Main URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
||||||
|
<setting id="update_repo_url_nightly" type="text" label="Nightly URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml" />
|
||||||
|
<setting id="update_repo_url" type="text" label="Custom URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
||||||
|
<setting id="auto_update_last_ts" type="text" label="Auto-Update letzte Pruefung (intern)" default="0" visible="false" />
|
||||||
|
<setting id="update_version_addon" type="text" label="ViewIT Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_serienstream" type="text" label="SerienStream Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_aniworld" type="text" label="AniWorld Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_einschalten" type="text" label="Einschalten Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_topstreamfilm" type="text" label="TopStream Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_filmpalast" type="text" label="Filmpalast Version" default="-" visible="false" />
|
||||||
|
<setting id="update_version_doku_streams" type="text" label="Doku-Streams Version" default="-" visible="false" />
|
||||||
|
</category>
|
||||||
|
|
||||||
|
<category label="Debug Global">
|
||||||
<setting id="debug_log_urls" type="bool" label="URLs mitschreiben (global)" default="false" />
|
<setting id="debug_log_urls" type="bool" label="URLs mitschreiben (global)" default="false" />
|
||||||
<setting id="debug_dump_html" type="bool" label="HTML speichern (global)" default="false" />
|
<setting id="debug_dump_html" type="bool" label="HTML speichern (global)" default="false" />
|
||||||
<setting id="debug_show_url_info" type="bool" label="Aktuelle URL anzeigen (global)" default="false" />
|
<setting id="debug_show_url_info" type="bool" label="Aktuelle URL anzeigen (global)" default="false" />
|
||||||
@@ -8,83 +68,32 @@
|
|||||||
<setting id="log_max_mb" type="number" label="URL-Log: maximale Dateigroesse (MB)" default="5" />
|
<setting id="log_max_mb" type="number" label="URL-Log: maximale Dateigroesse (MB)" default="5" />
|
||||||
<setting id="log_max_files" type="number" label="URL-Log: Anzahl alter Dateien" default="3" />
|
<setting id="log_max_files" type="number" label="URL-Log: Anzahl alter Dateien" default="3" />
|
||||||
<setting id="dump_max_files" type="number" label="HTML: maximale Dateien pro Plugin" default="200" />
|
<setting id="dump_max_files" type="number" label="HTML: maximale Dateien pro Plugin" default="200" />
|
||||||
<setting id="log_urls_serienstream" type="bool" label="Serienstream: URLs mitschreiben" default="false" />
|
</category>
|
||||||
<setting id="dump_html_serienstream" type="bool" label="Serienstream: HTML speichern" default="false" />
|
|
||||||
<setting id="show_url_info_serienstream" type="bool" label="Serienstream: Aktuelle URL anzeigen" default="false" />
|
<category label="Debug Quellen">
|
||||||
<setting id="log_errors_serienstream" type="bool" label="Serienstream: Fehler mitschreiben" default="false" />
|
<setting id="log_urls_serienstream" type="bool" label="SerienStream: URLs mitschreiben" default="false" />
|
||||||
<setting id="log_urls_aniworld" type="bool" label="Aniworld: URLs mitschreiben" default="false" />
|
<setting id="dump_html_serienstream" type="bool" label="SerienStream: HTML speichern" default="false" />
|
||||||
<setting id="dump_html_aniworld" type="bool" label="Aniworld: HTML speichern" default="false" />
|
<setting id="show_url_info_serienstream" type="bool" label="SerienStream: Aktuelle URL anzeigen" default="false" />
|
||||||
<setting id="show_url_info_aniworld" type="bool" label="Aniworld: Aktuelle URL anzeigen" default="false" />
|
<setting id="log_errors_serienstream" type="bool" label="SerienStream: Fehler mitschreiben" default="false" />
|
||||||
<setting id="log_errors_aniworld" type="bool" label="Aniworld: Fehler mitschreiben" default="false" />
|
|
||||||
<setting id="log_urls_topstreamfilm" type="bool" label="Topstreamfilm: URLs mitschreiben" default="false" />
|
<setting id="log_urls_aniworld" type="bool" label="AniWorld: URLs mitschreiben" default="false" />
|
||||||
<setting id="dump_html_topstreamfilm" type="bool" label="Topstreamfilm: HTML speichern" default="false" />
|
<setting id="dump_html_aniworld" type="bool" label="AniWorld: HTML speichern" default="false" />
|
||||||
<setting id="show_url_info_topstreamfilm" type="bool" label="Topstreamfilm: Aktuelle URL anzeigen" default="false" />
|
<setting id="show_url_info_aniworld" type="bool" label="AniWorld: Aktuelle URL anzeigen" default="false" />
|
||||||
<setting id="log_errors_topstreamfilm" type="bool" label="Topstreamfilm: Fehler mitschreiben" default="false" />
|
<setting id="log_errors_aniworld" type="bool" label="AniWorld: Fehler mitschreiben" default="false" />
|
||||||
|
|
||||||
|
<setting id="log_urls_topstreamfilm" type="bool" label="TopStream: URLs mitschreiben" default="false" />
|
||||||
|
<setting id="dump_html_topstreamfilm" type="bool" label="TopStream: HTML speichern" default="false" />
|
||||||
|
<setting id="show_url_info_topstreamfilm" type="bool" label="TopStream: Aktuelle URL anzeigen" default="false" />
|
||||||
|
<setting id="log_errors_topstreamfilm" type="bool" label="TopStream: Fehler mitschreiben" default="false" />
|
||||||
|
|
||||||
<setting id="log_urls_einschalten" type="bool" label="Einschalten: URLs mitschreiben" default="false" />
|
<setting id="log_urls_einschalten" type="bool" label="Einschalten: URLs mitschreiben" default="false" />
|
||||||
<setting id="dump_html_einschalten" type="bool" label="Einschalten: HTML speichern" default="false" />
|
<setting id="dump_html_einschalten" type="bool" label="Einschalten: HTML speichern" default="false" />
|
||||||
<setting id="show_url_info_einschalten" type="bool" label="Einschalten: Aktuelle URL anzeigen" default="false" />
|
<setting id="show_url_info_einschalten" type="bool" label="Einschalten: Aktuelle URL anzeigen" default="false" />
|
||||||
<setting id="log_errors_einschalten" type="bool" label="Einschalten: Fehler mitschreiben" default="false" />
|
<setting id="log_errors_einschalten" type="bool" label="Einschalten: Fehler mitschreiben" default="false" />
|
||||||
|
|
||||||
<setting id="log_urls_filmpalast" type="bool" label="Filmpalast: URLs mitschreiben" default="false" />
|
<setting id="log_urls_filmpalast" type="bool" label="Filmpalast: URLs mitschreiben" default="false" />
|
||||||
<setting id="dump_html_filmpalast" type="bool" label="Filmpalast: HTML speichern" default="false" />
|
<setting id="dump_html_filmpalast" type="bool" label="Filmpalast: HTML speichern" default="false" />
|
||||||
<setting id="show_url_info_filmpalast" type="bool" label="Filmpalast: Aktuelle URL anzeigen" default="false" />
|
<setting id="show_url_info_filmpalast" type="bool" label="Filmpalast: Aktuelle URL anzeigen" default="false" />
|
||||||
<setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" />
|
<setting id="log_errors_filmpalast" type="bool" label="Filmpalast: Fehler mitschreiben" default="false" />
|
||||||
</category>
|
</category>
|
||||||
<category label="TopStream">
|
|
||||||
<setting id="topstream_base_url" type="text" label="Basis-URL" default="https://topstreamfilm.live" />
|
|
||||||
<setting id="topstreamfilm_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
<setting id="topstream_genre_max_pages" type="number" label="Genres: max. Seiten laden" default="20" />
|
|
||||||
</category>
|
|
||||||
<category label="SerienStream">
|
|
||||||
<setting id="serienstream_base_url" type="text" label="Basis-URL" default="https://s.to" />
|
|
||||||
<setting id="serienstream_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
</category>
|
|
||||||
<category label="AniWorld">
|
|
||||||
<setting id="aniworld_base_url" type="text" label="Basis-URL" default="https://aniworld.to" />
|
|
||||||
<setting id="aniworld_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
</category>
|
|
||||||
<category label="Einschalten">
|
|
||||||
<setting id="einschalten_base_url" type="text" label="Basis-URL" default="https://einschalten.in" />
|
|
||||||
<setting id="einschalten_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
</category>
|
|
||||||
<category label="Filmpalast">
|
|
||||||
<setting id="filmpalast_base_url" type="text" label="Basis-URL" default="https://filmpalast.to" />
|
|
||||||
<setting id="filmpalast_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
</category>
|
|
||||||
<category label="Doku-Streams">
|
|
||||||
<setting id="doku_streams_base_url" type="text" label="Basis-URL" default="https://doku-streams.com" />
|
|
||||||
<setting id="doku_streams_metadata_source" type="enum" label="Metadatenquelle" default="0" values="Automatisch|Quelle|TMDB|Mischen" />
|
|
||||||
</category>
|
|
||||||
<category label="TMDB">
|
|
||||||
<setting id="tmdb_enabled" type="bool" label="TMDB aktivieren" default="true" />
|
|
||||||
<setting id="tmdb_api_key" type="text" label="TMDB API Key" default="" />
|
|
||||||
<setting id="tmdb_language" type="text" label="TMDB Sprache (z. B. de-DE)" default="de-DE" />
|
|
||||||
<setting id="tmdb_prefetch_concurrency" type="number" label="TMDB: gleichzeitige Anfragen (1-20)" default="6" />
|
|
||||||
<setting id="tmdb_show_plot" type="bool" label="TMDB Beschreibung anzeigen" default="true" />
|
|
||||||
<setting id="tmdb_show_art" type="bool" label="TMDB Poster und Vorschaubild anzeigen" default="true" />
|
|
||||||
<setting id="tmdb_show_fanart" type="bool" label="TMDB Fanart/Backdrop anzeigen" default="true" />
|
|
||||||
<setting id="tmdb_show_rating" type="bool" label="TMDB Bewertung anzeigen" default="true" />
|
|
||||||
<setting id="tmdb_show_votes" type="bool" label="TMDB Stimmen anzeigen" default="false" />
|
|
||||||
<setting id="tmdb_show_cast" type="bool" label="TMDB Besetzung anzeigen" default="false" />
|
|
||||||
<setting id="tmdb_show_episode_cast" type="bool" label="TMDB Besetzung pro Episode anzeigen" default="false" />
|
|
||||||
<setting id="tmdb_genre_metadata" type="bool" label="TMDB Daten in Genre-Listen anzeigen" default="false" />
|
|
||||||
<setting id="tmdb_log_requests" type="bool" label="TMDB API-Anfragen loggen" default="false" />
|
|
||||||
<setting id="tmdb_log_responses" type="bool" label="TMDB API-Antworten loggen" default="false" />
|
|
||||||
</category>
|
|
||||||
<category label="Update">
|
|
||||||
<setting id="update_channel" type="enum" label="Update-Kanal" default="0" values="Main|Nightly|Custom" />
|
|
||||||
<setting id="auto_update_enabled" type="bool" label="Automatische Updates (beim Start pruefen)" default="false" />
|
|
||||||
<setting id="update_repo_url_main" type="text" label="Main URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
|
||||||
<setting id="update_repo_url_nightly" type="text" label="Nightly URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/nightly/addons.xml" />
|
|
||||||
<setting id="update_repo_url" type="text" label="Custom URL (addons.xml)" default="https://gitea.it-drui.de/viewit/ViewIT-Kodi-Repo/raw/branch/main/addons.xml" />
|
|
||||||
<setting id="auto_update_last_ts" type="text" label="Auto-Update letzte Pruefung (intern)" default="0" visible="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="update_info" type="text" label="Updates laufen ueber den normalen Kodi-Update-Mechanismus." default="" enable="false" />
|
|
||||||
<setting id="update_version_addon" type="text" label="ViewIT Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_serienstream" type="text" label="Serienstream Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_aniworld" type="text" label="Aniworld Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_einschalten" type="text" label="Einschalten Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_topstreamfilm" type="text" label="Topstreamfilm Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_filmpalast" type="text" label="Filmpalast Version" default="-" enable="false" />
|
|
||||||
<setting id="update_version_doku_streams" type="text" label="Doku-Streams Version" default="-" enable="false" />
|
|
||||||
</category>
|
|
||||||
</settings>
|
</settings>
|
||||||
|
|||||||
Reference in New Issue
Block a user