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:
@@ -525,7 +525,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
return max_page
|
||||
|
||||
def capabilities(self) -> set[str]:
|
||||
return {"genres", "alpha", "series_catalog"}
|
||||
return {"genres", "alpha", "series_catalog", "popular_series", "latest_titles"}
|
||||
|
||||
def _parse_alpha_links(self, soup: BeautifulSoupT) -> Dict[str, str]:
|
||||
alpha: Dict[str, str] = {}
|
||||
@@ -726,7 +726,7 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
merged_poster = (poster or old_poster or "").strip()
|
||||
self._title_meta[title] = (merged_plot, merged_poster)
|
||||
|
||||
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str]:
|
||||
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str, str]:
|
||||
if not soup:
|
||||
return "", ""
|
||||
root = soup.select_one("div#content[role='main']") or soup
|
||||
@@ -773,7 +773,22 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
if "/themes/" not in lower and "spacer.gif" not in lower and "/files/movies/" in lower:
|
||||
poster = candidate
|
||||
|
||||
return plot, poster
|
||||
# IMDb-Rating: Schema.org aggregateRating
|
||||
rating = ""
|
||||
rating_node = detail.select_one("[itemprop='ratingValue']")
|
||||
if rating_node is not None:
|
||||
rating = (rating_node.get_text(" ", strip=True) or "").strip()
|
||||
if not rating:
|
||||
# Fallback: data-attribute oder Klassen-basierte Anzeige
|
||||
for sel in ("span.imdb", "span.rating", "[class*='imdb']"):
|
||||
node = detail.select_one(sel)
|
||||
if node is not None:
|
||||
candidate = (node.get_text(" ", strip=True) or "").strip()
|
||||
if candidate:
|
||||
rating = candidate
|
||||
break
|
||||
|
||||
return plot, poster, rating
|
||||
|
||||
def remember_series_url(self, title: str, series_url: str) -> None:
|
||||
title = (title or "").strip()
|
||||
@@ -830,12 +845,17 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
|
||||
try:
|
||||
soup = _get_soup(detail_url, session=get_requests_session("filmpalast", headers=HEADERS))
|
||||
plot, poster = self._extract_detail_metadata(soup)
|
||||
plot, poster, rating = self._extract_detail_metadata(soup)
|
||||
except Exception:
|
||||
plot, poster = "", ""
|
||||
plot, poster, rating = "", "", ""
|
||||
|
||||
if plot:
|
||||
info["plot"] = plot
|
||||
if rating:
|
||||
try:
|
||||
info["rating"] = str(float(rating.replace(",", ".")))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
if poster:
|
||||
art = {"thumb": poster, "poster": poster}
|
||||
self._store_title_meta(title, plot=info.get("plot", ""), poster=poster)
|
||||
@@ -1025,6 +1045,32 @@ class FilmpalastPlugin(BasisPlugin):
|
||||
def reset_preferred_hosters(self) -> None:
|
||||
self._preferred_hosters = list(self._default_preferred_hosters)
|
||||
|
||||
def popular_series(self) -> List[str]:
|
||||
"""Liefert beliebte Titel von /movies/top."""
|
||||
if not self._requests_available:
|
||||
return []
|
||||
try:
|
||||
url = _absolute_url("/movies/top")
|
||||
soup = _get_soup(url, session=get_requests_session("filmpalast", headers=HEADERS))
|
||||
hits = self._parse_listing_hits(soup)
|
||||
return self._apply_hits_to_title_index(hits)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def latest_titles(self, page: int = 1) -> List[str]:
|
||||
"""Liefert neu hinzugefuegte Titel von /movies/new."""
|
||||
if not self._requests_available:
|
||||
return []
|
||||
page = max(1, int(page or 1))
|
||||
try:
|
||||
base = _absolute_url("/movies/new")
|
||||
url = base if page == 1 else urljoin(base.rstrip("/") + "/", f"page/{page}")
|
||||
soup = _get_soup(url, session=get_requests_session("filmpalast", headers=HEADERS))
|
||||
hits = self._parse_listing_hits(soup)
|
||||
return self._apply_hits_to_title_index(hits)
|
||||
except Exception:
|
||||
return []
|
||||
|
||||
def resolve_stream_link(self, link: str) -> Optional[str]:
|
||||
if not link:
|
||||
return None
|
||||
|
||||
Reference in New Issue
Block a user