dev: bump to 0.1.74-dev – BurningSeries entfernt, Paging-Fix Neuste Titel

This commit is contained in:
2026-03-10 10:41:37 +01:00
parent 6e7b4c3d39
commit 5564851d35
10 changed files with 109 additions and 213 deletions

View File

@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.73-dev" provider-name="ViewIt">
<addon id="plugin.video.viewit" name="ViewIt" version="0.1.74-dev" provider-name="ViewIt">
<requires>
<import addon="xbmc.python" version="3.0.0" />
<import addon="script.module.requests" />

View File

@@ -1203,6 +1203,7 @@ def _normalize_update_info_url(raw: str) -> str:
UPDATE_CHANNEL_MAIN = 0
UPDATE_CHANNEL_NIGHTLY = 1
UPDATE_CHANNEL_CUSTOM = 2
UPDATE_CHANNEL_DEV = 3
_AUTO_UPDATE_INTERVALS = [1 * 60 * 60, 6 * 60 * 60, 24 * 60 * 60] # 1h, 6h, 24h
UPDATE_HTTP_TIMEOUT_SEC = 8
UPDATE_ADDON_ID = "plugin.video.viewit"
@@ -1212,7 +1213,7 @@ RESOLVEURL_AUTO_INSTALL_INTERVAL_SEC = 6 * 60 * 60
def _selected_update_channel() -> int:
channel = _get_setting_int("update_channel", default=UPDATE_CHANNEL_MAIN)
if channel not in {UPDATE_CHANNEL_MAIN, UPDATE_CHANNEL_NIGHTLY, UPDATE_CHANNEL_CUSTOM}:
if channel not in {UPDATE_CHANNEL_MAIN, UPDATE_CHANNEL_NIGHTLY, UPDATE_CHANNEL_CUSTOM, UPDATE_CHANNEL_DEV}:
return UPDATE_CHANNEL_MAIN
return channel
@@ -1222,6 +1223,8 @@ def _channel_label(channel: int) -> str:
return "Nightly"
if channel == UPDATE_CHANNEL_CUSTOM:
return "Custom"
if channel == UPDATE_CHANNEL_DEV:
return "Dev"
return "Main"
@@ -1246,11 +1249,17 @@ def _is_nightly_version(version: str) -> bool:
return bool(re.match(r"^\d+\.\d+\.\d+-nightly$", str(version or "").strip()))
def _is_dev_version(version: str) -> bool:
return bool(re.match(r"^\d+\.\d+\.\d+-dev$", str(version or "").strip()))
def _filter_versions_for_channel(channel: int, versions: list[str]) -> list[str]:
if channel == UPDATE_CHANNEL_MAIN:
return [v for v in versions if _is_stable_version(v)]
if channel == UPDATE_CHANNEL_NIGHTLY:
return [v for v in versions if _is_nightly_version(v)]
if channel == UPDATE_CHANNEL_DEV:
return [v for v in versions if _is_dev_version(v)]
return list(versions)
@@ -1260,6 +1269,8 @@ def _resolve_update_info_url() -> str:
raw = _get_setting_string("update_repo_url_nightly")
elif channel == UPDATE_CHANNEL_CUSTOM:
raw = _get_setting_string("update_repo_url")
elif channel == UPDATE_CHANNEL_DEV:
raw = _get_setting_string("update_repo_url_dev")
else:
raw = _get_setting_string("update_repo_url_main")
return _normalize_update_info_url(raw)
@@ -1661,24 +1672,6 @@ def _show_root_menu() -> None:
for plugin_name in sorted(plugins.keys(), key=lambda value: value.casefold()):
_add_directory_item(handle, plugin_name, "plugin_menu", {"plugin": plugin_name}, is_folder=True)
# Katalog-Caches im Hintergrund vorwaermen (fire-and-forget)
for _pname, _plugin in plugins.items():
_warmer = getattr(_plugin, "warm_catalog_cache", None)
if callable(_warmer):
def _warm_and_notify(_fn=_warmer, _name=_pname):
try:
loaded = _fn()
except Exception:
loaded = False
if loaded:
# executebuiltin ist thread-sicher; xbmcgui.Dialog() darf nicht
# aus Daemon-Threads aufgerufen werden (Kodi-Absturzgefahr).
safe_name = _name.replace('"', "")
xbmc.executebuiltin(
f'Notification("{safe_name}", "Suchindex geladen", 3000, "")'
)
threading.Thread(target=_warm_and_notify, daemon=True, name=f"viewit-warmup-{_pname}").start()
# Trakt-Menue (nur wenn aktiviert)
if _get_setting_bool("trakt_enabled", default=False):
if _trakt_load_token():
@@ -1704,7 +1697,8 @@ def _show_plugin_menu(plugin_name: str) -> None:
xbmcplugin.setPluginCategory(handle, plugin_name)
_add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True)
if callable(getattr(plugin, "search_titles", None)):
_add_directory_item(handle, "Suche", "plugin_search", {"plugin": plugin_name}, is_folder=True)
if _plugin_has_capability(plugin, "new_titles") or _plugin_has_capability(plugin, "latest_episodes"):
_add_directory_item(handle, LATEST_MENU_LABEL, "latest_titles", {"plugin": plugin_name, "page": "1"}, is_folder=True)
@@ -2527,34 +2521,6 @@ def _show_episodes(plugin_name: str, title: str, season: str, series_url: str =
xbmcplugin.endOfDirectory(handle)
def _show_genre_sources() -> None:
handle = _get_handle()
_log("Genre-Quellen laden.")
plugins = _discover_plugins()
sources: list[tuple[str, BasisPlugin]] = []
for plugin_name, plugin in plugins.items():
if plugin.__class__.genres is BasisPlugin.genres:
continue
if plugin.__class__.titles_for_genre is BasisPlugin.titles_for_genre:
continue
sources.append((plugin_name, plugin))
if not sources:
xbmcgui.Dialog().notification("Genres", "Keine Quellen mit Genres gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
return
for plugin_name, plugin in sources:
_add_directory_item(
handle,
f"Genres [{plugin_name}]",
"genres",
{"plugin": plugin_name},
is_folder=True,
)
xbmcplugin.endOfDirectory(handle)
def _show_genres(plugin_name: str) -> None:
handle = _get_handle()
_log(f"Genres laden: {plugin_name}")
@@ -2596,44 +2562,6 @@ def _show_genres(plugin_name: str) -> None:
xbmcplugin.endOfDirectory(handle)
def _show_categories(plugin_name: str) -> None:
handle = _get_handle()
_log(f"Kategorien laden: {plugin_name}")
plugin = _discover_plugins().get(plugin_name)
if plugin is None:
xbmcgui.Dialog().notification("Kategorien", "Quelle nicht gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
return
getter = getattr(plugin, "categories", None)
if not callable(getter):
xbmcgui.Dialog().notification("Kategorien", "Kategorien nicht verfuegbar.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
return
try:
categories = _run_with_progress(
"Kategorien",
f"{plugin_name}: Kategorien werden geladen...",
lambda: list(getter() or []),
)
except Exception as exc:
_log(f"Kategorien konnten nicht geladen werden ({plugin_name}): {exc}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification("Kategorien", "Kategorien konnten nicht geladen werden.", xbmcgui.NOTIFICATION_INFO, 3000)
xbmcplugin.endOfDirectory(handle)
return
for category in categories:
category = str(category).strip()
if not category:
continue
_add_directory_item(
handle,
category,
"category_titles_page",
{"plugin": plugin_name, "category": category, "page": "1"},
is_folder=True,
)
xbmcplugin.endOfDirectory(handle)
def _show_paged_title_list(
plugin_name: str,
filter_value: str,
@@ -2767,18 +2695,6 @@ def _show_paged_title_list(
xbmcplugin.endOfDirectory(handle)
def _show_category_titles_page(plugin_name: str, category: str, page: int = 1) -> None:
_show_paged_title_list(
plugin_name, category, page,
dialog_label="Kategorien",
page_action="category_titles_page",
filter_param="category",
paging_method="titles_for_genre_page",
count_method="genre_page_count",
has_more_method="genre_has_more",
)
def _show_genre_titles_page(plugin_name: str, genre: str, page: int = 1) -> None:
_show_paged_title_list(
plugin_name, genre, page,
@@ -3351,6 +3267,8 @@ def _show_new_titles(plugin_name: str, page: int = 1, *, action_name: str = "new
show_next = bool(has_more_getter(page))
except Exception:
show_next = False
elif callable(paging_getter) and page_items:
show_next = True
elif "total_pages" in locals():
show_next = bool(total_pages > 1 and page < total_pages) # type: ignore[name-defined]
@@ -3445,7 +3363,12 @@ def _show_latest_episodes(plugin_name: str, page: int = 1) -> None:
info_labels=info_labels,
)
if entries:
has_more_fn = getattr(plugin, "latest_episodes_has_more", None)
if callable(has_more_fn):
show_next = bool(has_more_fn(page))
else:
show_next = False
if show_next:
_add_directory_item(handle, "Naechste Seite", "latest_titles",
{"plugin": plugin_name, "page": str(page + 1)}, is_folder=True)
@@ -3842,6 +3765,51 @@ def _is_resolveurl_missing_error(message: str) -> bool:
return str(message or "").strip().casefold() == "resolveurl missing"
def _resolve_stream_with_retry(plugin: BasisPlugin, link: str) -> str | None:
"""Löst einen Stream-Link auf mit ResolveURL-Auto-Install und CF-Check.
Gibt den finalen Link zurück oder None (mit Kodi-Notification bei Fehler).
"""
resolved_link = plugin.resolve_stream_link(link)
if not resolved_link:
err = _resolveurl_last_error()
if _is_resolveurl_missing_error(err):
_log("ResolveURL fehlt: versuche Auto-Installation.", xbmc.LOGWARNING)
_ensure_resolveurl_installed(force=True, silent=True)
resolved_link = plugin.resolve_stream_link(link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err):
_log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
return None
if not resolved_link:
_log("Stream konnte nicht aufgeloest werden.", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Stream konnte nicht aufgeloest werden.",
xbmcgui.NOTIFICATION_INFO,
3000,
)
return None
final_link = normalize_resolved_stream_url(resolved_link, source_url=link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip():
_log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
return None
return final_link
def _play_final_link(
link: str,
*,
@@ -4086,43 +4054,8 @@ def _play_episode(
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
return
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
resolved_link = plugin.resolve_stream_link(link)
if not resolved_link:
err = _resolveurl_last_error()
if _is_resolveurl_missing_error(err):
_log("ResolveURL fehlt: versuche Auto-Installation.", xbmc.LOGWARNING)
_ensure_resolveurl_installed(force=True, silent=True)
resolved_link = plugin.resolve_stream_link(link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err):
_log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
return
if not resolved_link:
_log("Stream konnte nicht aufgeloest werden.", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Stream konnte nicht aufgeloest werden.",
xbmcgui.NOTIFICATION_INFO,
3000,
)
return
final_link = resolved_link
final_link = normalize_resolved_stream_url(final_link, source_url=link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip():
_log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
final_link = _resolve_stream_with_retry(plugin, link)
if not final_link:
return
finally:
if restore_hosters is not None and callable(preferred_setter):
@@ -4237,43 +4170,8 @@ def _play_episode_url(
xbmcgui.Dialog().notification("Wiedergabe", "Kein Stream gefunden.", xbmcgui.NOTIFICATION_INFO, 3000)
return
_log(f"Stream-Link: {link}", xbmc.LOGDEBUG)
resolved_link = plugin.resolve_stream_link(link)
if not resolved_link:
err = _resolveurl_last_error()
if _is_resolveurl_missing_error(err):
_log("ResolveURL fehlt: versuche Auto-Installation.", xbmc.LOGWARNING)
_ensure_resolveurl_installed(force=True, silent=True)
resolved_link = plugin.resolve_stream_link(link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err):
_log(f"ResolveURL Cloudflare-Challenge: {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
return
if not resolved_link:
_log("Stream konnte nicht aufgeloest werden.", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Stream konnte nicht aufgeloest werden.",
xbmcgui.NOTIFICATION_INFO,
3000,
)
return
final_link = resolved_link
final_link = normalize_resolved_stream_url(final_link, source_url=link)
err = _resolveurl_last_error()
if _is_cloudflare_challenge_error(err) and final_link.strip() == link.strip():
_log(f"ResolveURL Cloudflare-Challenge (unresolved): {err}", xbmc.LOGWARNING)
xbmcgui.Dialog().notification(
"Wiedergabe",
"Hoster durch Cloudflare geschuetzt. Bitte spaeter erneut probieren.",
xbmcgui.NOTIFICATION_INFO,
4500,
)
final_link = _resolve_stream_with_retry(plugin, link)
if not final_link:
return
finally:
if restore_hosters is not None and callable(preferred_setter):
@@ -4945,21 +4843,11 @@ def _route_plugin_search(params: dict[str, str]) -> None:
_show_plugin_search(params.get("plugin", ""))
@_router.route("genre_sources")
def _route_genre_sources(params: dict[str, str]) -> None:
_show_genre_sources()
@_router.route("genres")
def _route_genres(params: dict[str, str]) -> None:
_show_genres(params.get("plugin", ""))
@_router.route("categories")
def _route_categories(params: dict[str, str]) -> None:
_show_categories(params.get("plugin", ""))
@_router.route("latest_titles")
def _route_latest_titles(params: dict[str, str]) -> None:
_show_latest_titles(params.get("plugin", ""), _parse_positive_int(params.get("page", "1"), default=1))
@@ -4988,13 +4876,6 @@ def _route_genre_titles_page(params: dict[str, str]) -> None:
)
@_router.route("category_titles_page")
def _route_category_titles_page(params: dict[str, str]) -> None:
_show_category_titles_page(
params.get("plugin", ""), params.get("category", ""),
_parse_positive_int(params.get("page", "1"), default=1),
)
@_router.route("alpha_index")
def _route_alpha_index(params: dict[str, str]) -> None:

View File

@@ -9,7 +9,7 @@ Vorgehen fuer ein neues Plugin:
3. `search_titles`, `seasons_for`, `episodes_for` gemaess Zielseite implementieren
4. Optional weitere Methoden implementieren capabilities deklarieren und Methoden ueberschreiben:
- `popular_series()` + capability 'popular_series'
- `latest_titles(page)` + capability 'latest_titles'
- `new_titles()` + `new_titles_page(page)` + capability 'new_titles'
- `genres()` + `titles_for_genre(genre)` + `titles_for_genre_page(genre, page)`
- `alpha_index()` + `titles_for_alpha_page(letter, page)`
- `years_available()` + `titles_for_year(year, page)` + capability 'year_filter'
@@ -169,7 +169,7 @@ class TemplatePlugin(BasisPlugin):
Bekannte Werte (aus plugin_interface.py):
- 'popular_series' Plugin hat beliebte Serien/Filme
- 'latest_titles' Plugin hat neu hinzugefuegte Titel
- 'new_titles' Plugin hat neu hinzugefuegte Titel
- 'year_filter' Plugin unterstuetzt Jahr-Filter
- 'country_filter' Plugin unterstuetzt Land-Filter
- 'collections' Plugin hat Sammlungen/Filmreihen

View File

@@ -1025,7 +1025,7 @@ class AniworldPlugin(BasisPlugin):
_session_cache_set(self._season_episodes_cache_name(season_url), payload)
def capabilities(self) -> set[str]:
return {"popular_series", "genres", "latest_episodes", "latest_titles"}
return {"popular_series", "genres", "latest_episodes", "new_titles"}
def _find_series_by_title(self, title: str) -> Optional[SeriesResult]:
title = (title or "").strip()
@@ -1345,7 +1345,7 @@ class AniworldPlugin(BasisPlugin):
except Exception:
return 1
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles_page(self, page: int = 1) -> List[str]:
"""Liefert neu hinzugefuegte Anime vom Animekalender."""
if not self._requests_available:
return []
@@ -1372,6 +1372,9 @@ class AniworldPlugin(BasisPlugin):
except Exception:
return []
def new_titles(self) -> List[str]:
return self.new_titles_page(1)
def _season_label(self, number: int) -> str:
return f"Staffel {number}"

View File

@@ -1019,7 +1019,7 @@ class EinschaltenPlugin(BasisPlugin):
return resolve_via_resolveurl(link, fallback_to_link=True)
def capabilities(self) -> Set[str]:
return {"new_titles", "genres", "popular_series", "latest_titles"}
return {"new_titles", "genres", "popular_series"}
def popular_series(self) -> List[str]:
"""Liefert die am besten bewerteten Filme (nach voteAverage sortiert)."""
@@ -1042,14 +1042,6 @@ class EinschaltenPlugin(BasisPlugin):
titles.append(movie.title)
return titles
def latest_titles(self, page: int = 1) -> List[str]:
"""Liefert neu hinzugefügte Filme (Alias zu new_titles_page)."""
if not REQUESTS_AVAILABLE:
return []
if not self._get_base_url():
return []
return self.new_titles_page(max(1, int(page or 1)))
def new_titles(self) -> List[str]:
if not REQUESTS_AVAILABLE:
return []

View File

@@ -525,7 +525,7 @@ class FilmpalastPlugin(BasisPlugin):
return max_page
def capabilities(self) -> set[str]:
return {"genres", "alpha", "series_catalog", "popular_series", "latest_titles"}
return {"genres", "alpha", "series_catalog", "popular_series", "new_titles"}
def _parse_alpha_links(self, soup: BeautifulSoupT) -> Dict[str, str]:
alpha: Dict[str, str] = {}
@@ -1057,7 +1057,7 @@ class FilmpalastPlugin(BasisPlugin):
except Exception:
return []
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles_page(self, page: int = 1) -> List[str]:
"""Liefert neu hinzugefuegte Titel von /movies/new."""
if not self._requests_available:
return []
@@ -1071,6 +1071,9 @@ class FilmpalastPlugin(BasisPlugin):
except Exception:
return []
def new_titles(self) -> List[str]:
return self.new_titles_page(1)
def resolve_stream_link(self, link: str) -> Optional[str]:
if not link:
return None

View File

@@ -231,10 +231,13 @@ class NetzkinoPlugin(BasisPlugin):
# Browsing
# ------------------------------------------------------------------
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles_page(self, page: int = 1) -> List[str]:
url = _URL_CATEGORY.format(slug="neue-filme")
return self._load_posts(url)
def new_titles(self) -> List[str]:
return self.new_titles_page(1)
def genres(self) -> List[str]:
# Gibt die Anzeigenamen zurück (sortiert, Browsing-Kategorien)
return sorted(CATEGORIES.values())
@@ -248,4 +251,4 @@ class NetzkinoPlugin(BasisPlugin):
return self._load_posts(url)
def capabilities(self) -> set[str]:
return {"latest_titles", "genres"}
return {"new_titles", "genres"}

View File

@@ -1167,7 +1167,7 @@ class TopstreamfilmPlugin(BasisPlugin):
return resolve_via_resolveurl(link, fallback_to_link=True)
def capabilities(self) -> set[str]:
return {"genres", "popular_series", "year_filter", "latest_titles"}
return {"genres", "popular_series", "year_filter", "new_titles"}
def years_available(self) -> List[str]:
"""Liefert verfügbare Erscheinungsjahre (aktuelles Jahr bis 1980)."""
@@ -1207,7 +1207,7 @@ class TopstreamfilmPlugin(BasisPlugin):
self._save_title_url_cache()
return titles
def latest_titles(self, page: int = 1) -> List[str]:
def new_titles_page(self, page: int = 1) -> List[str]:
"""Liefert neu hinzugefügte Filme.
URL-Muster: /neueste-filme/ oder /neueste-filme/page/{n}/
@@ -1238,6 +1238,9 @@ class TopstreamfilmPlugin(BasisPlugin):
self._save_title_url_cache()
return titles
def new_titles(self) -> List[str]:
return self.new_titles_page(1)
# Alias für die automatische Plugin-Erkennung.
Plugin = TopstreamfilmPlugin