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

@@ -218,8 +218,10 @@ class TopstreamfilmPlugin(BasisPlugin):
if directory and not xbmcvfs.exists(directory):
xbmcvfs.mkdirs(directory)
handle = xbmcvfs.File(path, "w")
handle.write(payload)
handle.close()
try:
handle.write(payload)
finally:
handle.close()
else:
with open(path, "w", encoding="utf-8") as handle:
handle.write(payload)
@@ -283,8 +285,10 @@ class TopstreamfilmPlugin(BasisPlugin):
if directory and not xbmcvfs.exists(directory):
xbmcvfs.mkdirs(directory)
handle = xbmcvfs.File(path, "w")
handle.write(payload)
handle.close()
try:
handle.write(payload)
finally:
handle.close()
else:
with open(path, "w", encoding="utf-8") as handle:
handle.write(payload)
@@ -371,9 +375,6 @@ class TopstreamfilmPlugin(BasisPlugin):
message=message,
)
def capabilities(self) -> set[str]:
return {"genres", "popular_series"}
def _popular_url(self) -> str:
return self._absolute_url("/beliebte-filme-online.html")
@@ -1162,14 +1163,80 @@ class TopstreamfilmPlugin(BasisPlugin):
return hosters.get(first_name)
def resolve_stream_link(self, link: str) -> Optional[str]:
from plugin_helpers import resolve_via_resolveurl
return resolve_via_resolveurl(link, fallback_to_link=True)
def capabilities(self) -> set[str]:
return {"genres", "popular_series", "year_filter", "latest_titles"}
def years_available(self) -> List[str]:
"""Liefert verfügbare Erscheinungsjahre (aktuelles Jahr bis 1980)."""
import datetime
current_year = datetime.date.today().year
return [str(y) for y in range(current_year, 1979, -1)]
def titles_for_year(self, year: str, page: int = 1) -> List[str]:
"""Liefert Titel für ein bestimmtes Erscheinungsjahr.
URL-Muster: /xfsearch/{year}/ oder /xfsearch/{year}/page/{n}/
"""
year = (year or "").strip()
if not year or not REQUESTS_AVAILABLE or BeautifulSoup is None:
return []
page = max(1, int(page or 1))
base = self._get_base_url()
if page == 1:
url = f"{base}/xfsearch/{year}/"
else:
url = f"{base}/xfsearch/{year}/page/{page}/"
try:
from resolveurl_backend import resolve as resolve_with_resolveurl
soup = self._get_soup(url)
except Exception:
resolve_with_resolveurl = None
if callable(resolve_with_resolveurl):
resolved = resolve_with_resolveurl(link)
return resolved or link
return link
return []
hits = self._parse_listing_titles(soup)
titles: List[str] = []
seen: set[str] = set()
for hit in hits:
if hit.title in seen:
continue
seen.add(hit.title)
self._title_to_url[hit.title] = hit.url
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
titles.append(hit.title)
if titles:
self._save_title_url_cache()
return titles
def latest_titles(self, page: int = 1) -> List[str]:
"""Liefert neu hinzugefügte Filme.
URL-Muster: /neueste-filme/ oder /neueste-filme/page/{n}/
"""
if not REQUESTS_AVAILABLE or BeautifulSoup is None:
return []
page = max(1, int(page or 1))
base = self._get_base_url()
if page == 1:
url = f"{base}/neueste-filme/"
else:
url = f"{base}/neueste-filme/page/{page}/"
try:
soup = self._get_soup(url)
except Exception:
return []
hits = self._parse_listing_titles(soup)
titles: List[str] = []
seen: set[str] = set()
for hit in hits:
if hit.title in seen:
continue
seen.add(hit.title)
self._title_to_url[hit.title] = hit.url
self._store_title_meta(hit.title, plot=hit.description, poster=hit.poster)
titles.append(hit.title)
if titles:
self._save_title_url_cache()
return titles
# Alias für die automatische Plugin-Erkennung.