Unify menu labels, centralize hoster URL normalization, and add auto-update toggle

This commit is contained in:
2026-02-23 19:53:47 +01:00
parent d9e338c9b6
commit bd0bf34ae5
8 changed files with 286 additions and 279 deletions

View File

@@ -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.57" provider-name="ViewIt"> <addon id="plugin.video.viewit" name="ViewIt" version="0.1.58" 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" />
@@ -18,4 +18,4 @@
<license>GPL-3.0-or-later</license> <license>GPL-3.0-or-later</license>
<platform>all</platform> <platform>all</platform>
</extension> </extension>
</addon> </addon>

View File

@@ -18,6 +18,7 @@ import os
import re import re
import sys import sys
import threading import threading
import time
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
@@ -104,6 +105,7 @@ except ImportError: # pragma: no cover - allow importing outside Kodi (e.g. lin
from plugin_interface import BasisPlugin from plugin_interface import BasisPlugin
from http_session_pool import close_all_sessions from http_session_pool import close_all_sessions
from plugin_helpers import normalize_resolved_stream_url
from metadata_utils import ( from metadata_utils import (
collect_plugin_metadata as _collect_plugin_metadata, collect_plugin_metadata as _collect_plugin_metadata,
merge_metadata as _merge_metadata, merge_metadata as _merge_metadata,
@@ -127,6 +129,8 @@ _PLAYSTATE_CACHE: dict[str, dict[str, object]] | None = None
_PLAYSTATE_LOCK = threading.RLock() _PLAYSTATE_LOCK = threading.RLock()
_TMDB_LOCK = threading.RLock() _TMDB_LOCK = threading.RLock()
WATCHED_THRESHOLD = 0.9 WATCHED_THRESHOLD = 0.9
POPULAR_MENU_LABEL = "Haeufig gesehen"
LATEST_MENU_LABEL = "Neuste Titel"
atexit.register(close_all_sessions) atexit.register(close_all_sessions)
@@ -306,117 +310,26 @@ def _playstate_path() -> str:
def _load_playstate() -> dict[str, dict[str, object]]: def _load_playstate() -> dict[str, dict[str, object]]:
global _PLAYSTATE_CACHE return {}
with _PLAYSTATE_LOCK:
if _PLAYSTATE_CACHE is not None:
return _PLAYSTATE_CACHE
path = _playstate_path()
try:
if xbmcvfs and xbmcvfs.exists(path):
handle = xbmcvfs.File(path)
raw = handle.read()
handle.close()
else:
with open(path, "r", encoding="utf-8") as handle:
raw = handle.read()
data = json.loads(raw or "{}")
if isinstance(data, dict):
normalized: dict[str, dict[str, object]] = {}
for key, value in data.items():
if isinstance(key, str) and isinstance(value, dict):
normalized[key] = dict(value)
_PLAYSTATE_CACHE = normalized
return normalized
except Exception:
pass
_PLAYSTATE_CACHE = {}
return {}
def _save_playstate(state: dict[str, dict[str, object]]) -> None: def _save_playstate(state: dict[str, dict[str, object]]) -> None:
global _PLAYSTATE_CACHE return
with _PLAYSTATE_LOCK:
_PLAYSTATE_CACHE = state
path = _playstate_path()
try:
payload = json.dumps(state, ensure_ascii=False, sort_keys=True)
except Exception:
return
try:
if xbmcvfs:
directory = os.path.dirname(path)
if directory and not xbmcvfs.exists(directory):
xbmcvfs.mkdirs(directory)
handle = xbmcvfs.File(path, "w")
handle.write(payload)
handle.close()
else:
with open(path, "w", encoding="utf-8") as handle:
handle.write(payload)
except Exception:
return
def _get_playstate(key: str) -> dict[str, object]: def _get_playstate(key: str) -> dict[str, object]:
return dict(_load_playstate().get(key, {}) or {}) return {}
def _set_playstate(key: str, value: dict[str, object]) -> None: def _set_playstate(key: str, value: dict[str, object]) -> None:
state = _load_playstate() return
if value:
state[key] = dict(value)
else:
state.pop(key, None)
_save_playstate(state)
def _apply_playstate_to_info(info_labels: dict[str, object], playstate: dict[str, object]) -> dict[str, object]: def _apply_playstate_to_info(info_labels: dict[str, object], playstate: dict[str, object]) -> dict[str, object]:
info_labels = dict(info_labels or {}) return dict(info_labels or {})
watched = bool(playstate.get("watched") or False)
resume_position = playstate.get("resume_position")
resume_total = playstate.get("resume_total")
if watched:
info_labels["playcount"] = 1
info_labels.pop("resume_position", None)
info_labels.pop("resume_total", None)
else:
try:
pos = int(resume_position) if resume_position is not None else 0
tot = int(resume_total) if resume_total is not None else 0
except Exception:
pos, tot = 0, 0
if pos > 0 and tot > 0:
info_labels["resume_position"] = pos
info_labels["resume_total"] = tot
return info_labels
def _time_label(seconds: int) -> str:
try:
seconds = int(seconds or 0)
except Exception:
seconds = 0
if seconds <= 0:
return ""
hours = seconds // 3600
minutes = (seconds % 3600) // 60
secs = seconds % 60
if hours > 0:
return f"{hours:02d}:{minutes:02d}:{secs:02d}"
return f"{minutes:02d}:{secs:02d}"
def _label_with_playstate(label: str, playstate: dict[str, object]) -> str: def _label_with_playstate(label: str, playstate: dict[str, object]) -> str:
watched = bool(playstate.get("watched") or False)
if watched:
return f"[gesehen] {label}"
resume_pos = playstate.get("resume_position")
try:
pos = int(resume_pos) if resume_pos is not None else 0
except Exception:
pos = 0
if pos > 0:
return f"[fortsetzen {_time_label(pos)}] {label}"
return label return label
@@ -1059,6 +972,7 @@ def _normalize_update_info_url(raw: str) -> str:
UPDATE_CHANNEL_MAIN = 0 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
def _selected_update_channel() -> int: def _selected_update_channel() -> int:
@@ -1176,11 +1090,8 @@ def _show_plugin_menu(plugin_name: str) -> None:
_add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True) _add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True)
if _plugin_has_capability(plugin, "new_titles"): if _plugin_has_capability(plugin, "new_titles") or _plugin_has_capability(plugin, "latest_episodes"):
_add_directory_item(handle, "Neue Titel", "new_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True) _add_directory_item(handle, LATEST_MENU_LABEL, "latest_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True)
if _plugin_has_capability(plugin, "latest_episodes"):
_add_directory_item(handle, "Neueste Folgen", "latest_episodes", {"plugin": plugin_name, "page": "1"}, is_folder=True)
if _plugin_has_capability(plugin, "genres"): if _plugin_has_capability(plugin, "genres"):
_add_directory_item(handle, "Genres", "genres", {"plugin": plugin_name}, is_folder=True) _add_directory_item(handle, "Genres", "genres", {"plugin": plugin_name}, is_folder=True)
@@ -1192,7 +1103,7 @@ def _show_plugin_menu(plugin_name: str) -> None:
_add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True) _add_directory_item(handle, "Serien", "series_catalog", {"plugin": plugin_name, "page": "1"}, is_folder=True)
if _plugin_has_capability(plugin, "popular_series"): if _plugin_has_capability(plugin, "popular_series"):
_add_directory_item(handle, "Beliebte Serien", "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True) _add_directory_item(handle, POPULAR_MENU_LABEL, "popular", {"plugin": plugin_name, "page": "1"}, is_folder=True)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
@@ -2139,7 +2050,7 @@ def _show_category_titles_page(plugin_name: str, category: str, page: int = 1) -
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"category_titles_page", "category_titles_page",
{"plugin": plugin_name, "category": category, "page": str(page + 1)}, {"plugin": plugin_name, "category": category, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -2259,7 +2170,7 @@ def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"genre_titles_page", "genre_titles_page",
{"plugin": plugin_name, "genre": genre, "page": str(page + 1)}, {"plugin": plugin_name, "genre": genre, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -2412,7 +2323,7 @@ def _show_alpha_titles_page(plugin_name: str, letter: str, page: int = 1) -> Non
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"alpha_titles_page", "alpha_titles_page",
{"plugin": plugin_name, "letter": letter, "page": str(page + 1)}, {"plugin": plugin_name, "letter": letter, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -2531,7 +2442,7 @@ def _show_series_catalog(plugin_name: str, page: int = 1) -> None:
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"series_catalog", "series_catalog",
{"plugin": plugin_name, "page": str(page + 1)}, {"plugin": plugin_name, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -2669,14 +2580,14 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
if plugin_name: if plugin_name:
plugin = _discover_plugins().get(plugin_name) plugin = _discover_plugins().get(plugin_name)
if plugin is None or not _plugin_has_capability(plugin, "popular_series"): if plugin is None or not _plugin_has_capability(plugin, "popular_series"):
xbmcgui.Dialog().notification("Beliebte Serien", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
try: try:
popular_getter = getattr(plugin, "popular_series", None) popular_getter = getattr(plugin, "popular_series", None)
if callable(popular_getter): if callable(popular_getter):
titles = _run_with_progress( titles = _run_with_progress(
"Beliebte Serien", POPULAR_MENU_LABEL,
f"{plugin_name}: Liste wird geladen...", f"{plugin_name}: Liste wird geladen...",
lambda: list(popular_getter() or []), lambda: list(popular_getter() or []),
) )
@@ -2686,13 +2597,13 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
titles = [] titles = []
else: else:
titles = _run_with_progress( titles = _run_with_progress(
"Beliebte Serien", POPULAR_MENU_LABEL,
f"{plugin_name}: Liste wird geladen...", f"{plugin_name}: Liste wird geladen...",
lambda: list(plugin.titles_for_genre(label) or []), lambda: list(plugin.titles_for_genre(label) or []),
) )
except Exception as exc: except Exception as exc:
_log(f"Beliebte Serien konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING) _log(f"{POPULAR_MENU_LABEL} konnte nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification("Beliebte Serien", "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Serien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
@@ -2701,7 +2612,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
total = len(titles) total = len(titles)
total_pages = max(1, (total + page_size - 1) // page_size) total_pages = max(1, (total + page_size - 1) // page_size)
page = min(page, total_pages) page = min(page, total_pages)
xbmcplugin.setPluginCategory(handle, f"Beliebte Serien [{plugin_name}] ({page}/{total_pages})") xbmcplugin.setPluginCategory(handle, f"{POPULAR_MENU_LABEL} [{plugin_name}] ({page}/{total_pages})")
_set_content(handle, "tvshows") _set_content(handle, "tvshows")
if total_pages > 1 and page > 1: if total_pages > 1 and page > 1:
@@ -2735,7 +2646,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art):
tmdb_titles.append(title) tmdb_titles.append(title)
if show_tmdb and tmdb_titles: if show_tmdb and tmdb_titles:
with _busy_dialog("Beliebte Titel werden geladen..."): with _busy_dialog(f"{POPULAR_MENU_LABEL} wird geladen..."):
tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles)
for title in page_items: for title in page_items:
tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, []) tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, [])
@@ -2762,7 +2673,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
if total_pages > 1 and page < total_pages: if total_pages > 1 and page < total_pages:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"popular", "popular",
{"plugin": plugin_name, "page": str(page + 1)}, {"plugin": plugin_name, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -2772,15 +2683,15 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
sources = _plugins_with_popular() sources = _plugins_with_popular()
if not sources: if not sources:
xbmcgui.Dialog().notification("Beliebte Serien", "Keine Quellen gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(POPULAR_MENU_LABEL, "Keine Quellen gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
xbmcplugin.setPluginCategory(handle, "Beliebte Serien") xbmcplugin.setPluginCategory(handle, POPULAR_MENU_LABEL)
for name, plugin, _label in sources: for name, plugin, _label in sources:
_add_directory_item( _add_directory_item(
handle, handle,
f"Beliebte Serien [{plugin.name}]", f"{POPULAR_MENU_LABEL} [{plugin.name}]",
"popular", "popular",
{"plugin": name, "page": "1"}, {"plugin": name, "page": "1"},
is_folder=True, is_folder=True,
@@ -2788,7 +2699,7 @@ def _show_popular(plugin_name: str | None = None, page: int = 1) -> None:
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
def _show_new_titles(plugin_name: str, page: int = 1) -> None: def _show_new_titles(plugin_name: str, page: int = 1, *, action_name: str = "new_titles") -> None:
handle = _get_handle() handle = _get_handle()
page_size = 10 page_size = 10
page = max(1, int(page or 1)) page = max(1, int(page or 1))
@@ -2796,13 +2707,13 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
plugin_name = (plugin_name or "").strip() plugin_name = (plugin_name or "").strip()
plugin = _discover_plugins().get(plugin_name) plugin = _discover_plugins().get(plugin_name)
if plugin is None or not _plugin_has_capability(plugin, "new_titles"): if plugin is None or not _plugin_has_capability(plugin, "new_titles"):
xbmcgui.Dialog().notification("Neue Titel", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
getter = getattr(plugin, "new_titles", None) getter = getattr(plugin, "new_titles", None)
if not callable(getter): if not callable(getter):
xbmcgui.Dialog().notification("Neue Titel", "Diese Liste ist nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Liste ist nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
@@ -2810,25 +2721,25 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
has_more_getter = getattr(plugin, "new_titles_has_more", None) has_more_getter = getattr(plugin, "new_titles_has_more", None)
if callable(paging_getter): if callable(paging_getter):
xbmcplugin.setPluginCategory(handle, f"Neue Titel [{plugin_name}] ({page})") xbmcplugin.setPluginCategory(handle, f"{LATEST_MENU_LABEL} [{plugin_name}] ({page})")
_set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows") _set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows")
if page > 1: if page > 1:
_add_directory_item( _add_directory_item(
handle, handle,
"Vorherige Seite", "Vorherige Seite",
"new_titles", action_name,
{"plugin": plugin_name, "page": str(page - 1)}, {"plugin": plugin_name, "page": str(page - 1)},
is_folder=True, is_folder=True,
) )
try: try:
page_items = _run_with_progress( page_items = _run_with_progress(
"Neue Titel", LATEST_MENU_LABEL,
f"{plugin_name}: Seite {page} wird geladen...", f"{plugin_name}: Seite {page} wird geladen...",
lambda: list(paging_getter(page) or []), lambda: list(paging_getter(page) or []),
) )
except Exception as exc: except Exception as exc:
_log(f"Neue Titel konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING) _log(f"{LATEST_MENU_LABEL} konnten nicht geladen werden ({plugin_name} p{page}): {exc}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification("Neue Titel", "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
page_items = [str(t).strip() for t in page_items if t and str(t).strip()] page_items = [str(t).strip() for t in page_items if t and str(t).strip()]
@@ -2836,13 +2747,13 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
else: else:
try: try:
titles = _run_with_progress( titles = _run_with_progress(
"Neue Titel", LATEST_MENU_LABEL,
f"{plugin_name}: Liste wird geladen...", f"{plugin_name}: Liste wird geladen...",
lambda: list(getter() or []), lambda: list(getter() or []),
) )
except Exception as exc: except Exception as exc:
_log(f"Neue Titel konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING) _log(f"{LATEST_MENU_LABEL} konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification("Neue Titel", "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Titel konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
@@ -2851,21 +2762,21 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
total = len(titles) total = len(titles)
if total == 0: if total == 0:
xbmcgui.Dialog().notification( xbmcgui.Dialog().notification(
"Neue Titel", LATEST_MENU_LABEL,
"Keine Titel gefunden. Bitte Basis-URL oder Index pruefen.", "Keine Titel gefunden. Bitte Basis-URL oder Index pruefen.",
xbmcgui.NOTIFICATION_INFO, xbmcgui.NOTIFICATION_INFO,
4000, 4000,
) )
total_pages = max(1, (total + page_size - 1) // page_size) total_pages = max(1, (total + page_size - 1) // page_size)
page = min(page, total_pages) page = min(page, total_pages)
xbmcplugin.setPluginCategory(handle, f"Neue Titel [{plugin_name}] ({page}/{total_pages})") xbmcplugin.setPluginCategory(handle, f"{LATEST_MENU_LABEL} [{plugin_name}] ({page}/{total_pages})")
_set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows") _set_content(handle, "movies" if plugin_name.casefold() == "einschalten" else "tvshows")
if total_pages > 1 and page > 1: if total_pages > 1 and page > 1:
_add_directory_item( _add_directory_item(
handle, handle,
"Vorherige Seite", "Vorherige Seite",
"new_titles", action_name,
{"plugin": plugin_name, "page": str(page - 1)}, {"plugin": plugin_name, "page": str(page - 1)},
is_folder=True, is_folder=True,
) )
@@ -2891,7 +2802,7 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art): if _needs_tmdb(meta_labels, meta_art, want_plot=show_plot, want_art=show_art):
tmdb_titles.append(title) tmdb_titles.append(title)
if show_tmdb and tmdb_titles: if show_tmdb and tmdb_titles:
with _busy_dialog("Neue Titel werden geladen..."): with _busy_dialog(f"{LATEST_MENU_LABEL} wird geladen..."):
tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles) tmdb_prefetched = _tmdb_labels_and_art_bulk(tmdb_titles)
for title in page_items: for title in page_items:
tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, []) tmdb_info, tmdb_art, tmdb_cast = tmdb_prefetched.get(title, ({}, {}, [])) if show_tmdb else ({}, {}, [])
@@ -2930,8 +2841,8 @@ def _show_new_titles(plugin_name: str, page: int = 1) -> None:
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"new_titles", action_name,
{"plugin": plugin_name, "page": str(page + 1)}, {"plugin": plugin_name, "page": str(page + 1)},
is_folder=True, is_folder=True,
) )
@@ -2943,28 +2854,28 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None:
plugin_name = (plugin_name or "").strip() plugin_name = (plugin_name or "").strip()
plugin = _discover_plugins().get(plugin_name) plugin = _discover_plugins().get(plugin_name)
if not plugin: if not plugin:
xbmcgui.Dialog().notification("Neueste Folgen", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
getter = getattr(plugin, "latest_episodes", None) getter = getattr(plugin, "latest_episodes", None)
if not callable(getter): if not callable(getter):
xbmcgui.Dialog().notification("Neueste Folgen", "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
xbmcplugin.setPluginCategory(handle, f"{plugin_name}: Neueste Folgen") xbmcplugin.setPluginCategory(handle, f"{plugin_name}: {LATEST_MENU_LABEL}")
_set_content(handle, "episodes") _set_content(handle, "episodes")
try: try:
entries = _run_with_progress( entries = _run_with_progress(
"Neueste Folgen", LATEST_MENU_LABEL,
f"{plugin_name}: Seite {page} wird geladen...", f"{plugin_name}: Seite {page} wird geladen...",
lambda: list(getter(page) or []), lambda: list(getter(page) or []),
) )
except Exception as exc: except Exception as exc:
_log(f"Neueste Folgen fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING) _log(f"{LATEST_MENU_LABEL} fehlgeschlagen ({plugin_name}): {exc}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification("Neueste Folgen", "Abruf fehlgeschlagen.", xbmcgui.NOTIFICATION_INFO, 3000) xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Abruf fehlgeschlagen.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
return return
@@ -3017,6 +2928,25 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None:
xbmcplugin.endOfDirectory(handle) xbmcplugin.endOfDirectory(handle)
def _show_latest_titles(plugin_name: str, page: int = 1) -> None:
plugin_name = (plugin_name or "").strip()
plugin = _discover_plugins().get(plugin_name)
if plugin is None:
handle = _get_handle()
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
return
if _plugin_has_capability(plugin, "latest_episodes"):
_show_latest_episodes(plugin_name, page)
return
if _plugin_has_capability(plugin, "new_titles"):
_show_new_titles(plugin_name, page, action_name="latest_titles")
return
handle = _get_handle()
xbmcgui.Dialog().notification(LATEST_MENU_LABEL, "Diese Quelle bietet das nicht an.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page: int = 1) -> None: 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 = 10
@@ -3105,7 +3035,7 @@ def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page
if show_next: if show_next:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"genre_series_group", "genre_series_group",
{"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)}, {"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -3186,7 +3116,7 @@ def _show_genre_series_group(plugin_name: str, genre: str, group_code: str, page
if total_pages > 1 and page < total_pages: if total_pages > 1 and page < total_pages:
_add_directory_item( _add_directory_item(
handle, handle,
"Nächste Seite", "Naechste Seite",
"genre_series_group", "genre_series_group",
{"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)}, {"plugin": plugin_name, "genre": genre, "group": group_code, "page": str(page + 1)},
is_folder=True, is_folder=True,
@@ -3202,8 +3132,8 @@ def _open_settings() -> None:
addon.openSettings() addon.openSettings()
def _run_update_check() -> None: def _run_update_check(*, silent: bool = False) -> None:
"""Stoesst Kodi-Repo- und Addon-Updates an und informiert den Benutzer.""" """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:
@@ -3214,14 +3144,32 @@ def _run_update_check() -> None:
if callable(builtin): if callable(builtin):
builtin("UpdateAddonRepos") builtin("UpdateAddonRepos")
builtin("UpdateLocalAddons") builtin("UpdateLocalAddons")
builtin("ActivateWindow(addonbrowser,addons://updates/)") if not silent:
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000) builtin("ActivateWindow(addonbrowser,addons://updates/)")
if not silent:
xbmcgui.Dialog().notification("Updates", "Update-Check gestartet.", xbmcgui.NOTIFICATION_INFO, 4000)
except Exception as exc: except Exception as exc:
_log(f"Update-Pruefung fehlgeschlagen: {exc}", xbmc.LOGWARNING) _log(f"Update-Pruefung fehlgeschlagen: {exc}", xbmc.LOGWARNING)
try: if not silent:
xbmcgui.Dialog().notification("Updates", "Update-Check fehlgeschlagen.", xbmcgui.NOTIFICATION_ERROR, 4000) try:
except Exception: xbmcgui.Dialog().notification("Updates", "Update-Check fehlgeschlagen.", xbmcgui.NOTIFICATION_ERROR, 4000)
pass except Exception:
pass
def _maybe_run_auto_update_check(action: str | None) -> None:
action = (action or "").strip()
# Auto-Check nur beim Root-Menue, nicht in jedem Untermenue.
if action:
return
if not _get_setting_bool("auto_update_enabled", default=False):
return
now = int(time.time())
last = _get_setting_int("auto_update_last_ts", default=0)
if last > 0 and (now - last) < AUTO_UPDATE_INTERVAL_SEC:
return
_set_setting_string("auto_update_last_ts", str(now))
_run_update_check(silent=True)
def _extract_first_int(value: str) -> int | None: def _extract_first_int(value: str) -> int | None:
@@ -3295,81 +3243,12 @@ def _play_final_link(
def _track_playback_and_update_state(key: str) -> None: def _track_playback_and_update_state(key: str) -> None:
if not key: return
return
monitor = xbmc.Monitor() if xbmc is not None and hasattr(xbmc, "Monitor") else None
player = xbmc.Player()
# Wait for playback start.
started = False
for _ in range(30):
try:
if player.isPlayingVideo():
started = True
break
except Exception:
pass
if monitor and monitor.waitForAbort(0.5):
return
if not started:
return
last_pos = 0.0
total = 0.0
while True:
try:
if not player.isPlayingVideo():
break
last_pos = float(player.getTime() or 0.0)
total = float(player.getTotalTime() or 0.0)
except Exception:
pass
if monitor and monitor.waitForAbort(1.0):
return
if total <= 0.0:
return
percent = max(0.0, min(1.0, last_pos / total))
state: dict[str, object] = {"last_position": int(last_pos), "resume_total": int(total), "percent": percent}
if percent >= WATCHED_THRESHOLD:
state["watched"] = True
state["resume_position"] = 0
elif last_pos > 0:
state["watched"] = False
state["resume_position"] = int(last_pos)
_set_playstate(key, state)
# Zusätzlich aggregiert speichern, damit Titel-/Staffel-Listen "gesehen/fortsetzen"
# anzeigen können (für Filme/Serien gleichermaßen).
try:
parts = str(key).split("\t")
if len(parts) == 4:
plugin_name, title, season, _episode = parts
plugin_name = (plugin_name or "").strip()
title = (title or "").strip()
season = (season or "").strip()
if plugin_name and title:
_set_playstate(_playstate_key(plugin_name=plugin_name, title=title, season="", episode=""), state)
if season:
_set_playstate(_playstate_key(plugin_name=plugin_name, title=title, season=season, episode=""), state)
except Exception:
pass
def _track_playback_and_update_state_async(key: str) -> None: def _track_playback_and_update_state_async(key: str) -> None:
"""Startet Playstate-Tracking im Hintergrund, damit die UI nicht blockiert.""" # Eigenes Resume/Watched ist deaktiviert; Kodi verwaltet das selbst.
key = (key or "").strip() return
if not key:
return
def _worker() -> None:
try:
_track_playback_and_update_state(key)
except Exception:
pass
worker = threading.Thread(target=_worker, name="viewit-playstate-tracker", daemon=True)
worker.start()
def _play_episode( def _play_episode(
@@ -3456,6 +3335,7 @@ def _play_episode(
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 final_link = plugin.resolve_stream_link(link) or link
final_link = normalize_resolved_stream_url(final_link, source_url=link)
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)
@@ -3543,6 +3423,7 @@ def _play_episode_url(
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 final_link = plugin.resolve_stream_link(link) or link
final_link = normalize_resolved_stream_url(final_link, source_url=link)
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)
@@ -3582,6 +3463,7 @@ def run() -> None:
params = _parse_params() params = _parse_params()
action = params.get("action") action = params.get("action")
_log(f"Action: {action}", xbmc.LOGDEBUG) _log(f"Action: {action}", xbmc.LOGDEBUG)
_maybe_run_auto_update_check(action)
if action == "search": if action == "search":
_show_search() _show_search()
elif action == "plugin_menu": elif action == "plugin_menu":
@@ -3594,6 +3476,11 @@ def run() -> None:
_show_genres(params.get("plugin", "")) _show_genres(params.get("plugin", ""))
elif action == "categories": elif action == "categories":
_show_categories(params.get("plugin", "")) _show_categories(params.get("plugin", ""))
elif action == "latest_titles":
_show_latest_titles(
params.get("plugin", ""),
_parse_positive_int(params.get("page", "1"), default=1),
)
elif action == "new_titles": elif action == "new_titles":
_show_new_titles( _show_new_titles(
params.get("plugin", ""), params.get("plugin", ""),

View File

@@ -15,7 +15,9 @@ from __future__ import annotations
from datetime import datetime from datetime import datetime
import hashlib import hashlib
import os import os
import re
from typing import Optional from typing import Optional
from urllib.parse import parse_qsl, urlencode
try: # pragma: no cover - Kodi runtime try: # pragma: no cover - Kodi runtime
import xbmcaddon # type: ignore[import-not-found] import xbmcaddon # type: ignore[import-not-found]
@@ -237,3 +239,40 @@ def dump_response_html(
max_files = get_setting_int(addon_id, max_files_setting_id, default=200) max_files = get_setting_int(addon_id, max_files_setting_id, default=200)
_prune_dump_files(log_dir, prefix=filename_prefix, max_files=max_files) _prune_dump_files(log_dir, prefix=filename_prefix, max_files=max_files)
_append_text_file(path, content) _append_text_file(path, content)
def normalize_resolved_stream_url(final_url: str, *, source_url: str = "") -> str:
"""Normalisiert hoster-spezifische Header im finalen Stream-Link.
`final_url` kann ein Kodi-Header-Suffix enthalten: `url|Key=Value&...`.
Die Funktion passt nur bekannte Problemfaelle an und laesst sonst alles unveraendert.
"""
url = (final_url or "").strip()
if not url:
return ""
normalized = _normalize_supervideo_serversicuro(url, source_url=source_url)
return normalized
def _normalize_supervideo_serversicuro(final_url: str, *, source_url: str = "") -> str:
if "serversicuro.cc/hls/" not in final_url.casefold() or "|" not in final_url:
return final_url
source = (source_url or "").strip()
code_match = re.search(
r"supervideo\.(?:tv|cc)/(?:e/)?([a-z0-9]+)(?:\\.html)?",
source,
flags=re.IGNORECASE,
)
if not code_match:
return final_url
code = (code_match.group(1) or "").strip()
if not code:
return final_url
media_url, header_suffix = final_url.split("|", 1)
headers = dict(parse_qsl(header_suffix, keep_blank_values=True))
headers["Referer"] = f"https://supervideo.cc/e/{code}"
return f"{media_url}|{urlencode(headers)}"

View File

@@ -833,20 +833,55 @@ class AniworldPlugin(BasisPlugin):
merged_poster = (poster or old_poster or "").strip() merged_poster = (poster or old_poster or "").strip()
self._title_meta[title] = (merged_plot, merged_poster) self._title_meta[title] = (merged_plot, merged_poster)
def _extract_series_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]: @staticmethod
def _is_series_image_url(url: str) -> bool:
value = (url or "").strip().casefold()
if not value:
return False
blocked = (
"/public/img/facebook",
"/public/img/logo",
"aniworld-logo",
"favicon",
"/public/img/german.svg",
"/public/img/japanese-",
)
return not any(marker in value for marker in blocked)
@staticmethod
def _extract_style_url(style_value: str) -> str:
style_value = (style_value or "").strip()
if not style_value:
return ""
match = re.search(r"url\((['\"]?)(.*?)\1\)", style_value, flags=re.IGNORECASE)
if not match:
return ""
return (match.group(2) or "").strip()
def _extract_series_metadata(self, soup: BeautifulSoupT) -> tuple[str, str, str]:
if not soup: if not soup:
return "", "" return "", "", ""
plot = "" plot = ""
poster = "" poster = ""
fanart = ""
for selector in ("meta[property='og:description']", "meta[name='description']"): root = soup.select_one("#series") or soup
node = soup.select_one(selector)
if node is None: description_node = root.select_one("p.seri_des")
continue if description_node is not None:
content = (node.get("content") or "").strip() full_text = (description_node.get("data-full-description") or "").strip()
if content: short_text = (description_node.get_text(" ", strip=True) or "").strip()
plot = content plot = full_text or short_text
break
if not plot:
for selector in ("meta[property='og:description']", "meta[name='description']"):
node = soup.select_one(selector)
if node is None:
continue
content = (node.get("content") or "").strip()
if content:
plot = content
break
if not plot: if not plot:
for selector in (".series-description", ".seri_des", ".description", "article p"): for selector in (".series-description", ".seri_des", ".description", "article p"):
node = soup.select_one(selector) node = soup.select_one(selector)
@@ -857,25 +892,61 @@ class AniworldPlugin(BasisPlugin):
plot = text plot = text
break break
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"): cover = root.select_one("div.seriesCoverBox img[itemprop='image'], div.seriesCoverBox img")
node = soup.select_one(selector) if cover is not None:
if node is None: for attr in ("data-src", "src"):
continue value = (cover.get(attr) or "").strip()
content = (node.get("content") or "").strip() if value:
if content: candidate = _absolute_url(value)
poster = _absolute_url(content) if self._is_series_image_url(candidate):
break poster = candidate
break
if not poster: if not poster:
for selector in ("img.seriesCoverBox", ".seriesCoverBox img", "img[alt][src]"): for selector in ("meta[property='og:image']", "meta[name='twitter:image']"):
node = soup.select_one(selector)
if node is None:
continue
content = (node.get("content") or "").strip()
if content:
candidate = _absolute_url(content)
if self._is_series_image_url(candidate):
poster = candidate
break
if not poster:
for selector in ("img.seriesCoverBox", ".seriesCoverBox img"):
image = soup.select_one(selector) image = soup.select_one(selector)
if image is None: if image is None:
continue continue
value = (image.get("data-src") or image.get("src") or "").strip() value = (image.get("data-src") or image.get("src") or "").strip()
if value: if value:
poster = _absolute_url(value) candidate = _absolute_url(value)
break if self._is_series_image_url(candidate):
poster = candidate
break
return plot, poster backdrop_node = root.select_one("section.title .backdrop, .SeriesSection .backdrop, .backdrop")
if backdrop_node is not None:
raw_style = (backdrop_node.get("style") or "").strip()
style_url = self._extract_style_url(raw_style)
if style_url:
candidate = _absolute_url(style_url)
if self._is_series_image_url(candidate):
fanart = candidate
if not fanart:
for selector in ("meta[property='og:image']",):
node = soup.select_one(selector)
if node is None:
continue
content = (node.get("content") or "").strip()
if content:
candidate = _absolute_url(content)
if self._is_series_image_url(candidate):
fanart = candidate
break
return plot, poster, fanart
@staticmethod @staticmethod
def _season_links_cache_name(series_url: str) -> str: def _season_links_cache_name(series_url: str) -> str:
@@ -1031,14 +1102,17 @@ class AniworldPlugin(BasisPlugin):
try: try:
soup = _get_soup(series.url, session=get_requests_session("aniworld", headers=HEADERS)) soup = _get_soup(series.url, session=get_requests_session("aniworld", headers=HEADERS))
plot, poster = self._extract_series_metadata(soup) plot, poster, fanart = self._extract_series_metadata(soup)
except Exception: except Exception:
plot, poster = "", "" plot, poster, fanart = "", "", ""
if plot: if plot:
info["plot"] = plot info["plot"] = plot
if poster: if poster:
art = {"thumb": poster, "poster": poster} art = {"thumb": poster, "poster": poster}
if fanart:
art["fanart"] = fanart
art["landscape"] = fanart
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster) self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
return info, art, None return info, art, None

View File

@@ -735,44 +735,49 @@ class FilmpalastPlugin(BasisPlugin):
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]: def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
if not soup: if not soup:
return "", "" return "", ""
root = soup.select_one("div#content[role='main']") or soup
detail = root.select_one("article.detail") or root
plot = "" plot = ""
poster = "" poster = ""
for selector in ("meta[property='og:description']", "meta[name='description']"): # Filmpalast Detailseite: bevorzugt den dedizierten Filmhandlung-Block.
node = soup.select_one(selector) plot_node = detail.select_one(
if node is None: "li[itemtype='http://schema.org/Movie'] span[itemprop='description']"
continue )
content = (node.get("content") or "").strip() if plot_node is not None:
if content: plot = (plot_node.get_text(" ", strip=True) or "").strip()
plot = content
break
if not plot: if not plot:
for selector in (".toggle-content .coverDetails", ".entry-content p", "article p"): hidden_plot = detail.select_one("cite span.hidden")
node = soup.select_one(selector) if hidden_plot is not None:
plot = (hidden_plot.get_text(" ", strip=True) or "").strip()
if not plot:
for selector in ("meta[property='og:description']", "meta[name='description']"):
node = root.select_one(selector)
if node is None: if node is None:
continue continue
text = (node.get_text(" ", strip=True) or "").strip() content = (node.get("content") or "").strip()
if text and len(text) > 40: if content:
plot = text plot = content
break break
for selector in ("meta[property='og:image']", "meta[name='twitter:image']"): # Filmpalast Detailseite: Cover liegt stabil in `img.cover2`.
node = soup.select_one(selector) cover = detail.select_one("img.cover2")
if node is None: if cover is not None:
continue value = (cover.get("data-src") or cover.get("src") or "").strip()
content = (node.get("content") or "").strip() if value:
if content: candidate = _absolute_url(value)
poster = _absolute_url(content) lower = candidate.casefold()
break if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
poster = candidate
if not poster: if not poster:
for selector in ("img.cover", "article img", ".entry-content img"): thumb_node = detail.select_one("li[itemtype='http://schema.org/Movie'] img[itemprop='image']")
image = soup.select_one(selector) if thumb_node is not None:
if image is None: value = (thumb_node.get("data-src") or thumb_node.get("src") or "").strip()
continue
value = (image.get("data-src") or image.get("src") or "").strip()
if value: if value:
poster = _absolute_url(value) candidate = _absolute_url(value)
break lower = candidate.casefold()
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
poster = candidate
return plot, poster return plot, poster

View File

@@ -1096,7 +1096,7 @@ class SerienstreamPlugin(BasisPlugin):
name = "Serienstream" name = "Serienstream"
version = "1.0.0" version = "1.0.0"
POPULAR_GENRE_LABEL = "⭐ Beliebte Serien" POPULAR_GENRE_LABEL = "Haeufig gesehen"
def __init__(self) -> None: def __init__(self) -> None:
self._series_results: Dict[str, SeriesResult] = {} self._series_results: Dict[str, SeriesResult] = {}

View File

@@ -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 urlencode, urljoin from urllib.parse import urljoin
try: # pragma: no cover - optional dependency try: # pragma: no cover - optional dependency
import requests import requests

View File

@@ -72,9 +72,11 @@
</category> </category>
<category label="Update"> <category label="Update">
<setting id="update_channel" type="enum" label="Update-Kanal" default="0" values="Main|Nightly|Custom" /> <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_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_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="http://127.0.0.1:8080/repo/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="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_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_addon" type="text" label="ViewIT Version" default="-" enable="false" />