Unify menu labels, centralize hoster URL normalization, and add auto-update toggle
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
331
addon/default.py
331
addon/default.py
@@ -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", ""),
|
||||||
|
|||||||
@@ -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)}"
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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] = {}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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" />
|
||||||
|
|||||||
Reference in New Issue
Block a user