dev: umfangreiches Refactoring, Trakt-Integration und Code-Review-Fixes (0.1.69-dev)

Core & Architektur:
- Neues Verzeichnis addon/core/ mit router.py, trakt.py, metadata.py,
  gui.py, playstate.py, plugin_manager.py, updater.py
- Tests-Verzeichnis hinzugefügt (24 Tests, pytest + Coverage)

Trakt-Integration:
- OAuth Device Flow, Scrobbling, Watchlist, History, Calendar
- Upcoming Episodes, Weiterschauen (Continue Watching)
- Watched-Status in Episodenlisten
- _trakt_find_in_plugins() mit 5-Min-Cache

Serienstream-Suche:
- API-Ergebnisse werden immer mit Katalog-Cache ergänzt (serverseitiges 10-Treffer-Limit)
- Katalog-Cache wird beim Addon-Start im Daemon-Thread vorgewärmt
- Notification nach Cache-Load via xbmc.executebuiltin() (thread-sicher)

Bugfixes (Code-Review):
- Race Condition auf _TRAKT_WATCHED_CACHE: _TRAKT_WATCHED_CACHE_LOCK hinzugefügt
- GUI-Dialog aus Daemon-Thread: xbmcgui -> xbmc.executebuiltin()
- ValueError in Trakt-Watchlist-Routen abgesichert
- Token expires_at==0 Check korrigiert
- get_setting_bool() Kontrollfluss in gui.py bereinigt
- topstreamfilm_plugin: try-finally um xbmcvfs.File.close()

Cleanup:
- default.py.bak und refactor_router.py entfernt
- .gitignore: /tests/ Eintrag entfernt
- Type-Hints vereinheitlicht (Dict/List/Tuple -> dict/list/tuple)
This commit is contained in:
2026-03-01 18:23:45 +01:00
parent 73f07d20b4
commit 7b60b00c8b
36 changed files with 4765 additions and 672 deletions

View File

@@ -1015,16 +1015,40 @@ class EinschaltenPlugin(BasisPlugin):
return stream_url or None
def resolve_stream_link(self, link: str) -> Optional[str]:
try:
from resolveurl_backend import resolve as resolve_with_resolveurl
except Exception:
resolve_with_resolveurl = None
if callable(resolve_with_resolveurl):
return resolve_with_resolveurl(link) or link
return link
from plugin_helpers import resolve_via_resolveurl
return resolve_via_resolveurl(link, fallback_to_link=True)
def capabilities(self) -> Set[str]:
return {"new_titles", "genres"}
return {"new_titles", "genres", "popular_series", "latest_titles"}
def popular_series(self) -> List[str]:
"""Liefert die am besten bewerteten Filme (nach voteAverage sortiert)."""
if not REQUESTS_AVAILABLE:
return []
if not self._get_base_url():
return []
movies = self._load_movies()
with_rating = [m for m in movies if m.vote_average is not None]
without_rating = [m for m in movies if m.vote_average is None]
ranked = sorted(with_rating, key=lambda m: (m.vote_average or 0.0), reverse=True)
ordered = ranked + without_rating
titles: List[str] = []
seen: set[str] = set()
for movie in ordered[:50]:
if movie.title in seen:
continue
seen.add(movie.title)
self._id_by_title[movie.title] = movie.id
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: