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:
@@ -54,12 +54,26 @@ class TraktMediaIds:
|
|||||||
class TraktItem:
|
class TraktItem:
|
||||||
title: str
|
title: str
|
||||||
year: int
|
year: int
|
||||||
media_type: str # "movie" oder "show"
|
media_type: str # "movie", "show" oder "episode"
|
||||||
ids: TraktMediaIds = field(default_factory=TraktMediaIds)
|
ids: TraktMediaIds = field(default_factory=TraktMediaIds)
|
||||||
season: int = 0
|
season: int = 0
|
||||||
episode: int = 0
|
episode: int = 0
|
||||||
watched_at: str = ""
|
watched_at: str = ""
|
||||||
poster: 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)
|
@dataclass(frozen=True)
|
||||||
@@ -71,9 +85,27 @@ class TraktCalendarItem:
|
|||||||
season: int
|
season: int
|
||||||
episode: int
|
episode: int
|
||||||
episode_title: str
|
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"
|
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
|
# Client
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -332,7 +364,7 @@ class TraktClient:
|
|||||||
path = "/users/me/history"
|
path = "/users/me/history"
|
||||||
if media_type in ("movies", "shows", "episodes"):
|
if media_type in ("movies", "shows", "episodes"):
|
||||||
path = f"{path}/{media_type}"
|
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)
|
status, payload = self._get(path, token=token)
|
||||||
if status != 200 or not isinstance(payload, list):
|
if status != 200 or not isinstance(payload, list):
|
||||||
return []
|
return []
|
||||||
@@ -351,7 +383,7 @@ class TraktClient:
|
|||||||
if not start_date:
|
if not start_date:
|
||||||
from datetime import date
|
from datetime import date
|
||||||
start_date = date.today().strftime("%Y-%m-%d")
|
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)
|
status, payload = self._get(path, token=token)
|
||||||
if status != 200 or not isinstance(payload, list):
|
if status != 200 or not isinstance(payload, list):
|
||||||
return []
|
return []
|
||||||
@@ -362,6 +394,13 @@ class TraktClient:
|
|||||||
show = entry.get("show") or {}
|
show = entry.get("show") or {}
|
||||||
ep = entry.get("episode") or {}
|
ep = entry.get("episode") or {}
|
||||||
show_ids = self._parse_ids(show.get("ids") 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(
|
items.append(TraktCalendarItem(
|
||||||
show_title=str(show.get("title", "") or ""),
|
show_title=str(show.get("title", "") or ""),
|
||||||
show_year=int(show.get("year", 0) or 0),
|
show_year=int(show.get("year", 0) or 0),
|
||||||
@@ -369,10 +408,75 @@ class TraktClient:
|
|||||||
season=int(ep.get("season", 0) or 0),
|
season=int(ep.get("season", 0) or 0),
|
||||||
episode=int(ep.get("number", 0) or 0),
|
episode=int(ep.get("number", 0) or 0),
|
||||||
episode_title=str(ep.get("title", "") or ""),
|
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 ""),
|
first_aired=str(entry.get("first_aired", "") or ""),
|
||||||
))
|
))
|
||||||
return items
|
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
|
# Parser
|
||||||
# -------------------------------------------------------------------
|
# -------------------------------------------------------------------
|
||||||
@@ -417,6 +521,13 @@ class TraktClient:
|
|||||||
show = entry.get("show") or {}
|
show = entry.get("show") or {}
|
||||||
ep = entry.get("episode") or {}
|
ep = entry.get("episode") or {}
|
||||||
ids = self._parse_ids((show.get("ids") 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(
|
result.append(TraktItem(
|
||||||
title=str(show.get("title", "") or ""),
|
title=str(show.get("title", "") or ""),
|
||||||
year=int(show.get("year", 0) or 0),
|
year=int(show.get("year", 0) or 0),
|
||||||
@@ -425,6 +536,11 @@ class TraktClient:
|
|||||||
season=int(ep.get("season", 0) or 0),
|
season=int(ep.get("season", 0) or 0),
|
||||||
episode=int(ep.get("number", 0) or 0),
|
episode=int(ep.get("number", 0) or 0),
|
||||||
watched_at=watched_at,
|
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:
|
else:
|
||||||
media = entry.get("movie") or entry.get("show") or {}
|
media = entry.get("movie") or entry.get("show") or {}
|
||||||
|
|||||||
172
addon/default.py
172
addon/default.py
@@ -131,6 +131,7 @@ _MEDIA_TYPE_CACHE: dict[str, str] = {}
|
|||||||
_TMDB_SEASON_CACHE: dict[tuple[int, int, str, str], dict[int, tuple[dict[str, str], dict[str, str]]]] = {}
|
_TMDB_SEASON_CACHE: dict[tuple[int, int, str, str], dict[int, tuple[dict[str, str], dict[str, str]]]] = {}
|
||||||
_TMDB_SEASON_SUMMARY_CACHE: dict[tuple[int, int, str, str], tuple[dict[str, str], dict[str, str]]] = {}
|
_TMDB_SEASON_SUMMARY_CACHE: dict[tuple[int, int, str, str], tuple[dict[str, str], dict[str, str]]] = {}
|
||||||
_TMDB_EPISODE_CAST_CACHE: dict[tuple[int, int, int, str], list[TmdbCastMember]] = {}
|
_TMDB_EPISODE_CAST_CACHE: dict[tuple[int, int, int, str], list[TmdbCastMember]] = {}
|
||||||
|
_TRAKT_SEASON_META_CACHE: dict = {}
|
||||||
_TMDB_LOG_PATH: str | None = None
|
_TMDB_LOG_PATH: str | None = None
|
||||||
_GENRE_TITLES_CACHE: dict[tuple[str, str], list[str]] = {}
|
_GENRE_TITLES_CACHE: dict[tuple[str, str], list[str]] = {}
|
||||||
_ADDON_INSTANCE = None
|
_ADDON_INSTANCE = None
|
||||||
@@ -987,7 +988,9 @@ def _tmdb_episode_labels_and_art(*, title: str, season_label: str, episode_label
|
|||||||
_tmdb_labels_and_art(title)
|
_tmdb_labels_and_art(title)
|
||||||
tmdb_id = _tmdb_cache_get(_TMDB_ID_CACHE, title_key)
|
tmdb_id = _tmdb_cache_get(_TMDB_ID_CACHE, title_key)
|
||||||
if not tmdb_id:
|
if not tmdb_id:
|
||||||
return {"title": episode_label}, {}
|
return _trakt_episode_labels_and_art(
|
||||||
|
title=title, season_label=season_label, episode_label=episode_label
|
||||||
|
)
|
||||||
|
|
||||||
season_number = _extract_first_int(season_label)
|
season_number = _extract_first_int(season_label)
|
||||||
episode_number = _extract_first_int(episode_label)
|
episode_number = _extract_first_int(episode_label)
|
||||||
@@ -1003,7 +1006,9 @@ def _tmdb_episode_labels_and_art(*, title: str, season_label: str, episode_label
|
|||||||
if cached_season is None:
|
if cached_season is None:
|
||||||
api_key = _get_setting_string("tmdb_api_key").strip()
|
api_key = _get_setting_string("tmdb_api_key").strip()
|
||||||
if not api_key or api_key == "None":
|
if not api_key or api_key == "None":
|
||||||
return {"title": episode_label}, {}
|
return _trakt_episode_labels_and_art(
|
||||||
|
title=title, season_label=season_label, episode_label=episode_label
|
||||||
|
)
|
||||||
log_requests = _get_setting_bool("tmdb_log_requests", default=False)
|
log_requests = _get_setting_bool("tmdb_log_requests", default=False)
|
||||||
log_responses = _get_setting_bool("tmdb_log_responses", default=False)
|
log_responses = _get_setting_bool("tmdb_log_responses", default=False)
|
||||||
log_fn = _tmdb_file_log if (log_requests or log_responses) else None
|
log_fn = _tmdb_file_log if (log_requests or log_responses) else None
|
||||||
@@ -1038,6 +1043,66 @@ def _tmdb_episode_labels_and_art(*, title: str, season_label: str, episode_label
|
|||||||
return cached_season.get(episode_number, ({"title": episode_label}, {}))
|
return cached_season.get(episode_number, ({"title": episode_label}, {}))
|
||||||
|
|
||||||
|
|
||||||
|
def _trakt_episode_labels_and_art(
|
||||||
|
*, title: str, season_label: str, episode_label: str
|
||||||
|
) -> tuple[dict[str, str], dict[str, str]]:
|
||||||
|
"""Trakt-Fallback für Episoden-Metadaten wenn TMDB nicht verfügbar.
|
||||||
|
Lädt Staffel-Episodendaten per Batch (extended=full,images) und optionale
|
||||||
|
deutsche Übersetzung per Episode (Translations-Endpunkt).
|
||||||
|
"""
|
||||||
|
client = _trakt_get_client()
|
||||||
|
if not client:
|
||||||
|
return {"title": episode_label}, {}
|
||||||
|
season_number = _extract_first_int(season_label)
|
||||||
|
episode_number = _extract_first_int(episode_label)
|
||||||
|
if season_number is None or episode_number is None:
|
||||||
|
return {"title": episode_label}, {}
|
||||||
|
|
||||||
|
cache_key = (title.strip().casefold(), season_number)
|
||||||
|
cached = _tmdb_cache_get(_TRAKT_SEASON_META_CACHE, cache_key)
|
||||||
|
if cached is None:
|
||||||
|
slug = client.search_show(title)
|
||||||
|
if not slug:
|
||||||
|
_tmdb_cache_set(_TRAKT_SEASON_META_CACHE, cache_key, {})
|
||||||
|
return {"title": episode_label}, {}
|
||||||
|
meta = client.lookup_tv_season(slug, season_number)
|
||||||
|
_tmdb_cache_set(_TRAKT_SEASON_META_CACHE, cache_key, {"slug": slug, "episodes": meta or {}})
|
||||||
|
cached = _tmdb_cache_get(_TRAKT_SEASON_META_CACHE, cache_key)
|
||||||
|
|
||||||
|
slug = (cached or {}).get("slug", "")
|
||||||
|
episodes: dict = (cached or {}).get("episodes", {})
|
||||||
|
ep = episodes.get(episode_number)
|
||||||
|
if not ep:
|
||||||
|
return {"title": episode_label}, {}
|
||||||
|
|
||||||
|
ep_title = ep.title or episode_label
|
||||||
|
ep_overview = ep.overview
|
||||||
|
|
||||||
|
language = _get_setting_string("tmdb_language").strip() or "de-DE"
|
||||||
|
lang_code = language[:2]
|
||||||
|
if slug and lang_code and lang_code != "en":
|
||||||
|
trans_key = (cache_key, episode_number, lang_code)
|
||||||
|
trans_cached = _tmdb_cache_get(_TRAKT_SEASON_META_CACHE, trans_key)
|
||||||
|
if trans_cached is None:
|
||||||
|
t_title, t_overview = client.get_episode_translation(slug, season_number, episode_number, lang_code)
|
||||||
|
trans_cached = {"title": t_title, "overview": t_overview}
|
||||||
|
_tmdb_cache_set(_TRAKT_SEASON_META_CACHE, trans_key, trans_cached)
|
||||||
|
if trans_cached.get("title"):
|
||||||
|
ep_title = trans_cached["title"]
|
||||||
|
if trans_cached.get("overview"):
|
||||||
|
ep_overview = trans_cached["overview"]
|
||||||
|
|
||||||
|
info: dict[str, str] = {"title": ep_title}
|
||||||
|
if ep_overview:
|
||||||
|
info["plot"] = ep_overview
|
||||||
|
if ep.runtime_minutes:
|
||||||
|
info["duration"] = str(ep.runtime_minutes * 60)
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
if ep.thumb:
|
||||||
|
art["thumb"] = ep.thumb
|
||||||
|
return info, art
|
||||||
|
|
||||||
|
|
||||||
def _tmdb_episode_cast(*, title: str, season_label: str, episode_label: str) -> list[TmdbCastMember]:
|
def _tmdb_episode_cast(*, title: str, season_label: str, episode_label: str) -> list[TmdbCastMember]:
|
||||||
if not _tmdb_enabled():
|
if not _tmdb_enabled():
|
||||||
return []
|
return []
|
||||||
@@ -4526,12 +4591,29 @@ def _show_trakt_watchlist(media_type: str = "") -> None:
|
|||||||
xbmcplugin.endOfDirectory(handle)
|
xbmcplugin.endOfDirectory(handle)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
_set_content(handle, "tvshows")
|
||||||
items = client.get_watchlist(token, media_type=media_type)
|
items = client.get_watchlist(token, media_type=media_type)
|
||||||
for item in items:
|
for item in items:
|
||||||
label = f"{item.title}"
|
label = f"{item.title} ({item.year})" if item.year else item.title
|
||||||
|
|
||||||
|
tmdb_info, art, _ = _tmdb_labels_and_art(item.title)
|
||||||
|
info_labels: dict[str, object] = dict(tmdb_info)
|
||||||
|
info_labels["title"] = label
|
||||||
|
info_labels["tvshowtitle"] = item.title
|
||||||
if item.year:
|
if item.year:
|
||||||
label = f"{item.title} ({item.year})"
|
info_labels["year"] = item.year
|
||||||
_add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True)
|
info_labels["mediatype"] = "tvshow"
|
||||||
|
|
||||||
|
match = _trakt_find_in_plugins(item.title)
|
||||||
|
if match:
|
||||||
|
plugin_name, matched_title = match
|
||||||
|
action = "seasons"
|
||||||
|
params: dict[str, str] = {"plugin": plugin_name, "title": matched_title}
|
||||||
|
else:
|
||||||
|
action = "search"
|
||||||
|
params = {"query": item.title}
|
||||||
|
|
||||||
|
_add_directory_item(handle, label, action, params, is_folder=True, info_labels=info_labels, art=art)
|
||||||
if not items:
|
if not items:
|
||||||
xbmcgui.Dialog().notification("Trakt", "Watchlist ist leer.", xbmcgui.NOTIFICATION_INFO, 3000)
|
xbmcgui.Dialog().notification("Trakt", "Watchlist ist leer.", xbmcgui.NOTIFICATION_INFO, 3000)
|
||||||
xbmcplugin.endOfDirectory(handle)
|
xbmcplugin.endOfDirectory(handle)
|
||||||
@@ -4546,14 +4628,66 @@ def _show_trakt_history(page: int = 1) -> None:
|
|||||||
xbmcplugin.endOfDirectory(handle)
|
xbmcplugin.endOfDirectory(handle)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
xbmcplugin.setPluginCategory(handle, "Trakt: Zuletzt gesehen")
|
||||||
|
_set_content(handle, "episodes")
|
||||||
|
|
||||||
items = client.get_history(token, page=page, limit=LIST_PAGE_SIZE)
|
items = client.get_history(token, page=page, limit=LIST_PAGE_SIZE)
|
||||||
for item in items:
|
for item in items:
|
||||||
label = item.title
|
is_episode = item.media_type == "episode" and item.season and item.episode
|
||||||
if item.media_type == "episode" and item.season and item.episode:
|
|
||||||
label = f"{item.title} - S{item.season:02d}E{item.episode:02d}"
|
# Label mit Episodentitel wenn vorhanden
|
||||||
|
if is_episode:
|
||||||
|
ep_title = item.episode_title or f"Episode {item.episode}"
|
||||||
|
label = f"{item.title} – S{item.season:02d}E{item.episode:02d}: {ep_title}"
|
||||||
elif item.year:
|
elif item.year:
|
||||||
label = f"{item.title} ({item.year})"
|
label = f"{item.title} ({item.year})"
|
||||||
_add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True)
|
else:
|
||||||
|
label = item.title
|
||||||
|
|
||||||
|
# Artwork: Trakt-Bilder als Basis, TMDB ergänzt fehlende Keys
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
if item.episode_thumb:
|
||||||
|
art["thumb"] = item.episode_thumb
|
||||||
|
if item.show_fanart:
|
||||||
|
art["fanart"] = item.show_fanart
|
||||||
|
if item.show_poster:
|
||||||
|
art["poster"] = item.show_poster
|
||||||
|
_, tmdb_art, _ = _tmdb_labels_and_art(item.title)
|
||||||
|
for _k, _v in tmdb_art.items():
|
||||||
|
art.setdefault(_k, _v)
|
||||||
|
|
||||||
|
# Info-Labels
|
||||||
|
info_labels: dict[str, object] = {}
|
||||||
|
info_labels["title"] = item.episode_title if is_episode and item.episode_title else label
|
||||||
|
info_labels["tvshowtitle"] = item.title
|
||||||
|
if item.year:
|
||||||
|
info_labels["year"] = item.year
|
||||||
|
if is_episode:
|
||||||
|
info_labels["season"] = item.season
|
||||||
|
info_labels["episode"] = item.episode
|
||||||
|
if item.episode_overview:
|
||||||
|
info_labels["plot"] = item.episode_overview
|
||||||
|
info_labels["mediatype"] = "episode" if is_episode else "tvshow"
|
||||||
|
|
||||||
|
# Navigation: Episoden direkt abspielen, Serien zur Staffelauswahl
|
||||||
|
match = _trakt_find_in_plugins(item.title)
|
||||||
|
if match:
|
||||||
|
plugin_name, matched_title = match
|
||||||
|
if is_episode:
|
||||||
|
action = "play_episode"
|
||||||
|
params: dict[str, str] = {
|
||||||
|
"plugin": plugin_name,
|
||||||
|
"title": matched_title,
|
||||||
|
"season": f"Staffel {item.season}",
|
||||||
|
"episode": f"Episode {item.episode}",
|
||||||
|
}
|
||||||
|
_add_directory_item(handle, label, action, params, is_folder=False, info_labels=info_labels, art=art)
|
||||||
|
else:
|
||||||
|
action = "seasons"
|
||||||
|
params = {"plugin": plugin_name, "title": matched_title}
|
||||||
|
_add_directory_item(handle, label, action, params, is_folder=True, info_labels=info_labels, art=art)
|
||||||
|
else:
|
||||||
|
_add_directory_item(handle, label, "search", {"query": item.title}, is_folder=True, info_labels=info_labels, art=art)
|
||||||
|
|
||||||
if len(items) >= LIST_PAGE_SIZE:
|
if len(items) >= LIST_PAGE_SIZE:
|
||||||
_add_directory_item(handle, "Naechste Seite >>", "trakt_history", {"page": str(page + 1)}, is_folder=True)
|
_add_directory_item(handle, "Naechste Seite >>", "trakt_history", {"page": str(page + 1)}, is_folder=True)
|
||||||
@@ -4613,8 +4747,20 @@ def _show_trakt_upcoming() -> None:
|
|||||||
}
|
}
|
||||||
if item.show_year:
|
if item.show_year:
|
||||||
info_labels["year"] = item.show_year
|
info_labels["year"] = item.show_year
|
||||||
|
if item.episode_overview:
|
||||||
|
info_labels["plot"] = item.episode_overview
|
||||||
|
|
||||||
_, art, _ = _tmdb_labels_and_art(item.show_title)
|
# Artwork: Trakt-Bilder als Basis, TMDB ergänzt fehlende Keys
|
||||||
|
art: dict[str, str] = {}
|
||||||
|
if item.episode_thumb:
|
||||||
|
art["thumb"] = item.episode_thumb
|
||||||
|
if item.show_fanart:
|
||||||
|
art["fanart"] = item.show_fanart
|
||||||
|
if item.show_poster:
|
||||||
|
art["poster"] = item.show_poster
|
||||||
|
_, tmdb_art, _ = _tmdb_labels_and_art(item.show_title)
|
||||||
|
for _k, _v in tmdb_art.items():
|
||||||
|
art.setdefault(_k, _v)
|
||||||
|
|
||||||
match = _trakt_find_in_plugins(item.show_title)
|
match = _trakt_find_in_plugins(item.show_title)
|
||||||
if match:
|
if match:
|
||||||
@@ -4736,7 +4882,11 @@ def _show_trakt_continue_watching() -> None:
|
|||||||
|
|
||||||
@_router.route("search")
|
@_router.route("search")
|
||||||
def _route_search(params: dict[str, str]) -> None:
|
def _route_search(params: dict[str, str]) -> None:
|
||||||
_show_search()
|
query = params.get("query", "").strip()
|
||||||
|
if query:
|
||||||
|
_show_search_results(query)
|
||||||
|
else:
|
||||||
|
_show_search()
|
||||||
|
|
||||||
|
|
||||||
@_router.route("plugin_menu")
|
@_router.route("plugin_menu")
|
||||||
|
|||||||
@@ -593,6 +593,7 @@ def resolve_redirect(target_url: str) -> Optional[str]:
|
|||||||
response = None
|
response = None
|
||||||
try:
|
try:
|
||||||
response = session.get(normalized_url, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
|
response = session.get(normalized_url, headers=HEADERS, timeout=DEFAULT_TIMEOUT, allow_redirects=True)
|
||||||
|
response.raise_for_status()
|
||||||
if response.url:
|
if response.url:
|
||||||
_log_url(response.url, kind="RESOLVED")
|
_log_url(response.url, kind="RESOLVED")
|
||||||
return response.url if response.url else None
|
return response.url if response.url else None
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ class DokuStreamsPlugin(BasisPlugin):
|
|||||||
|
|
||||||
def clean_name(value: str) -> str:
|
def clean_name(value: str) -> str:
|
||||||
value = (value or "").strip()
|
value = (value or "").strip()
|
||||||
return re.sub(r"\\s*\\(\\d+\\)\\s*$", "", value).strip()
|
return re.sub(r"\s*\(\d+\)\s*$", "", value).strip()
|
||||||
|
|
||||||
def walk(ul, parents: List[str]) -> None:
|
def walk(ul, parents: List[str]) -> None:
|
||||||
for li in ul.find_all("li", recursive=False):
|
for li in ul.find_all("li", recursive=False):
|
||||||
|
|||||||
@@ -728,7 +728,7 @@ class FilmpalastPlugin(BasisPlugin):
|
|||||||
|
|
||||||
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str, str]:
|
def _extract_detail_metadata(self, soup: BeautifulSoupT) -> tuple[str, str, str]:
|
||||||
if not soup:
|
if not soup:
|
||||||
return "", ""
|
return "", "", ""
|
||||||
root = soup.select_one("div#content[role='main']") or soup
|
root = soup.select_one("div#content[role='main']") or soup
|
||||||
detail = root.select_one("article.detail") or root
|
detail = root.select_one("article.detail") or root
|
||||||
plot = ""
|
plot = ""
|
||||||
|
|||||||
@@ -865,7 +865,7 @@ def _extract_episodes(soup: BeautifulSoupT) -> list[EpisodeInfo]:
|
|||||||
onclick = (row.get("onclick") or "").strip()
|
onclick = (row.get("onclick") or "").strip()
|
||||||
url = ""
|
url = ""
|
||||||
if onclick:
|
if onclick:
|
||||||
match = re.search(r"location=['\\\"]([^'\\\"]+)['\\\"]", onclick)
|
match = re.search(r"location=['\"]([^'\"]+)['\"]", onclick)
|
||||||
if match:
|
if match:
|
||||||
url = _absolute_url(match.group(1))
|
url = _absolute_url(match.group(1))
|
||||||
if not url:
|
if not url:
|
||||||
@@ -923,8 +923,6 @@ def _extract_episodes(soup: BeautifulSoupT) -> list[EpisodeInfo]:
|
|||||||
hosters=hosters,
|
hosters=hosters,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
if episodes:
|
|
||||||
return episodes
|
|
||||||
return episodes
|
return episodes
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -39,4 +39,18 @@ else
|
|||||||
find "${DEST_DIR}" -type f -name '*.pyc' -delete || true
|
find "${DEST_DIR}" -type f -name '*.pyc' -delete || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Auch nach ~/.kodi/addons/ deployen wenn vorhanden
|
||||||
|
KODI_ADDON_DIR="${HOME}/.kodi/addons/${ADDON_ID}"
|
||||||
|
if [[ -d "${HOME}/.kodi/addons" ]]; then
|
||||||
|
if command -v rsync >/dev/null 2>&1; then
|
||||||
|
rsync -a --delete \
|
||||||
|
--exclude '__pycache__/' \
|
||||||
|
--exclude '*.pyc' \
|
||||||
|
"${DEST_DIR}/" "${KODI_ADDON_DIR}/"
|
||||||
|
else
|
||||||
|
rm -rf "${KODI_ADDON_DIR}"
|
||||||
|
cp -a "${DEST_DIR}" "${KODI_ADDON_DIR}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
echo "${DEST_DIR}"
|
echo "${DEST_DIR}"
|
||||||
|
|||||||
Reference in New Issue
Block a user