dev: bump to 0.1.71-dev – Trakt History direkt abspielen, Metadaten + Plugin-Bugfixes

- Trakt History: Episoden starten direkt (kein Staffel-Dialog mehr)
- Trakt History: Episodentitel, Plot und Artwork bereits in der Übersicht
- TraktItem um episode_title, episode_overview, episode_thumb, show_poster, show_fanart erweitert
- get_history() nutzt jetzt ?extended=full,images
- Slash-Commands /check und /deploy angelegt
- build_install_addon.sh deployt jetzt auch nach ~/.kodi/addons/
- filmpalast_plugin: return-Tuple-Bug gefixt (return "", "", "")
- dokustreams_plugin: Regex-Escaping für clean_name() korrigiert
- aniworld_plugin: raise_for_status() in resolve_redirect() ergänzt
- serienstream_plugin: Toter Code und unnötigen Regex-Backslash entfernt
This commit is contained in:
2026-03-01 22:56:51 +01:00
parent 95e14583e0
commit ff30548811
7 changed files with 298 additions and 19 deletions

View File

@@ -54,12 +54,26 @@ class TraktMediaIds:
class TraktItem:
title: str
year: int
media_type: str # "movie" oder "show"
media_type: str # "movie", "show" oder "episode"
ids: TraktMediaIds = field(default_factory=TraktMediaIds)
season: int = 0
episode: int = 0
watched_at: str = ""
poster: str = ""
episode_title: str = "" # Episodentitel (extended=full)
episode_overview: str = "" # Episoden-Inhaltsangabe (extended=full)
episode_thumb: str = "" # Screenshot-URL (extended=images)
show_poster: str = "" # Serien-Poster-URL (extended=images)
show_fanart: str = "" # Serien-Fanart-URL (extended=images)
@dataclass(frozen=True)
class TraktEpisodeMeta:
"""Metadaten einer einzelnen Episode (aus extended=full,images)."""
title: str
overview: str
runtime_minutes: int
thumb: str # Screenshot-URL (https://)
@dataclass(frozen=True)
@@ -71,9 +85,27 @@ class TraktCalendarItem:
season: int
episode: int
episode_title: str
episode_overview: str # Episoden-Inhaltsangabe (extended=full)
episode_thumb: str # Screenshot-URL (https://)
show_poster: str # Poster-URL (https://)
show_fanart: str # Fanart-URL (https://)
first_aired: str # ISO-8601, z.B. "2026-03-02T02:00:00.000Z"
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _trakt_image_url(raw: str) -> str:
"""Stellt https:// vor relative Trakt-Bild-URLs."""
if not raw:
return ""
raw = raw.strip()
if raw.startswith("http"):
return raw
return f"https://{raw}"
# ---------------------------------------------------------------------------
# Client
# ---------------------------------------------------------------------------
@@ -332,7 +364,7 @@ class TraktClient:
path = "/users/me/history"
if media_type in ("movies", "shows", "episodes"):
path = f"{path}/{media_type}"
path = f"{path}?page={page}&limit={limit}"
path = f"{path}?page={page}&limit={limit}&extended=full,images"
status, payload = self._get(path, token=token)
if status != 200 or not isinstance(payload, list):
return []
@@ -351,7 +383,7 @@ class TraktClient:
if not start_date:
from datetime import date
start_date = date.today().strftime("%Y-%m-%d")
path = f"/calendars/my/shows/{start_date}/{days}"
path = f"/calendars/my/shows/{start_date}/{days}?extended=full,images"
status, payload = self._get(path, token=token)
if status != 200 or not isinstance(payload, list):
return []
@@ -362,6 +394,13 @@ class TraktClient:
show = entry.get("show") or {}
ep = entry.get("episode") or {}
show_ids = self._parse_ids(show.get("ids") or {})
ep_images = ep.get("images") or {}
show_images = show.get("images") or {}
def _first(img_dict: dict, key: str) -> str:
imgs = img_dict.get(key) or []
return _trakt_image_url(imgs[0]) if imgs else ""
items.append(TraktCalendarItem(
show_title=str(show.get("title", "") or ""),
show_year=int(show.get("year", 0) or 0),
@@ -369,10 +408,75 @@ class TraktClient:
season=int(ep.get("season", 0) or 0),
episode=int(ep.get("number", 0) or 0),
episode_title=str(ep.get("title", "") or ""),
episode_overview=str(ep.get("overview", "") or ""),
episode_thumb=_first(ep_images, "screenshot"),
show_poster=_first(show_images, "poster"),
show_fanart=_first(show_images, "fanart"),
first_aired=str(entry.get("first_aired", "") or ""),
))
return items
def search_show(self, query: str) -> str:
"""GET /search/show?query=... gibt slug des ersten Treffers zurück, sonst ''."""
from urllib.parse import urlencode
path = f"/search/show?{urlencode({'query': query, 'limit': 1})}"
status, payload = self._get(path)
if status != 200 or not isinstance(payload, list) or not payload:
return ""
show = (payload[0] or {}).get("show") or {}
ids = show.get("ids") or {}
return str(ids.get("slug") or ids.get("trakt") or "")
def lookup_tv_season(
self,
show_id_or_slug: "str | int",
season_number: int,
*,
token: str = "",
) -> "dict[int, TraktEpisodeMeta] | None":
"""GET /shows/{id}/seasons/{n}/episodes?extended=full,images
Gibt episode_number -> TraktEpisodeMeta zurück, oder None bei Fehler.
"""
path = f"/shows/{show_id_or_slug}/seasons/{season_number}/episodes?extended=full,images"
status, payload = self._get(path, token=token)
if status != 200 or not isinstance(payload, list):
return None
result: "dict[int, TraktEpisodeMeta]" = {}
for entry in payload:
try:
ep_no = int(entry.get("number") or 0)
except Exception:
continue
if not ep_no:
continue
images = entry.get("images") or {}
screenshots = images.get("screenshot") or []
thumb = _trakt_image_url(screenshots[0]) if screenshots else ""
result[ep_no] = TraktEpisodeMeta(
title=str(entry.get("title") or "").strip(),
overview=str(entry.get("overview") or "").strip(),
runtime_minutes=int(entry.get("runtime") or 0),
thumb=thumb,
)
return result or None
def get_episode_translation(
self,
show_id_or_slug: "str | int",
season: int,
episode: int,
language: str = "de",
) -> "tuple[str, str]":
"""GET /shows/{id}/seasons/{s}/episodes/{e}/translations/{lang}
Gibt (title, overview) in der Zielsprache zurück, oder ('', '') bei Fehler.
"""
path = f"/shows/{show_id_or_slug}/seasons/{season}/episodes/{episode}/translations/{language}"
status, payload = self._get(path)
if status != 200 or not isinstance(payload, list) or not payload:
return "", ""
first = payload[0] if payload else {}
return str(first.get("title") or ""), str(first.get("overview") or "")
# -------------------------------------------------------------------
# Parser
# -------------------------------------------------------------------
@@ -417,6 +521,13 @@ class TraktClient:
show = entry.get("show") or {}
ep = entry.get("episode") or {}
ids = self._parse_ids((show.get("ids") or {}))
ep_images = ep.get("images") or {}
show_images = show.get("images") or {}
def _first_img(img_dict: dict, key: str) -> str:
imgs = img_dict.get(key) or []
return _trakt_image_url(imgs[0]) if imgs else ""
result.append(TraktItem(
title=str(show.get("title", "") or ""),
year=int(show.get("year", 0) or 0),
@@ -425,6 +536,11 @@ class TraktClient:
season=int(ep.get("season", 0) or 0),
episode=int(ep.get("number", 0) or 0),
watched_at=watched_at,
episode_title=str(ep.get("title", "") or ""),
episode_overview=str(ep.get("overview", "") or ""),
episode_thumb=_first_img(ep_images, "screenshot"),
show_poster=_first_img(show_images, "poster"),
show_fanart=_first_img(show_images, "fanart"),
))
else:
media = entry.get("movie") or entry.get("show") or {}