# coding: utf-8

import common
import sys
import urllib.request
import urllib.parse
import json
import xbmc
import xbmcgui
import xbmcaddon
import xbmcplugin
import os
import xbmcvfs
import ssl
import hashlib
import datetime as dt_module
import random
import traceback
import re

# Optional: paralleles Bild-Caching
try:
    from concurrent.futures import ThreadPoolExecutor, as_completed
    _HAS_FUTURES = True
except Exception:
    _HAS_FUTURES = False


# ---------------------------------------------------------------------------------------
# Konfiguration / Konstanten
# ---------------------------------------------------------------------------------------

SERIES_FAV_JSON_FILENAME = "seriesFav.json"
LOCAL_IMAGE_CACHE_SUBDIR_FAV = "cache_fav_series_images"
LOCAL_IMAGE_CACHE_DIR_FAV = ""

# Log-Helper
LOGP = f"[{common.addon_id} favorites.py]"

def log(msg, lvl=xbmc.LOGINFO):
    try:
        xbmc.log(f"{LOGP} {msg}", lvl)
    except Exception:
        pass


def start_series_monitor(playable_path: str, identifier_for_status: str = None):
    """
    Startet den Playback-Monitor-Thread für eine Serienepisode.
    Nutzt series.monitor_playback_series(playable_path, identifier_for_status).
    """
    try:
        if not playable_path:
            return
        import threading
        import series

        ident = (identifier_for_status or playable_path or "").strip() or playable_path

        t = threading.Thread(
            target=series.monitor_playback_series,
            args=(playable_path, ident)
        )
        t.daemon = True
        t.start()
        log(f"Random-Monitor gestartet für: {playable_path} (id={ident})", xbmc.LOGDEBUG)
    except Exception as e:
        log(f"Random-Monitor Fehler: {e}", xbmc.LOGDEBUG)

# ---------------------------------------------------------------------------------------
# Safe-Getter für Addon-Settings (ohne settings.xml)
# ---------------------------------------------------------------------------------------
def _get_setting_str(key, default=""):
    try:
        val = common.addon.getSetting(key)
        return val if isinstance(val, str) else default
    except Exception:
        return default

def _get_setting_bool(key, default=False):
    try:
        raw = common.addon.getSetting(key)
    except Exception:
        raw = ""
    s = (raw or "").strip().lower()
    if s in ("true", "1", "yes", "on"):
        return True
    if s in ("false", "0", "no", "off"):
        return False
    return default

def _get_setting_int(key, default=0):
    try:
        raw = common.addon.getSetting(key)
        return int(raw)
    except Exception:
        return default


# ---------------------------------------------------------------------------------------
# Netz / Timeouts / SSL
# ---------------------------------------------------------------------------------------
NET_TIMEOUT = max(5, _get_setting_int("net_timeout", 20))
IMG_TIMEOUT = max(5, _get_setting_int("image_timeout", 15))

PARALLEL_IMAGES = _get_setting_bool("image_cache_parallel", True)
IMAGE_WORKERS = max(1, min(8, _get_setting_int("image_workers", 4)))

try:
    _SSL_CTX = ssl.create_default_context()
except Exception:
    _SSL_CTX = None

def _robust_urlopen(req, timeout=NET_TIMEOUT):
    if _SSL_CTX:
        return urllib.request.urlopen(req, context=_SSL_CTX, timeout=timeout)
    return urllib.request.urlopen(req, timeout=timeout)

def unquote2(s):
    return urllib.parse.unquote(urllib.parse.unquote(s or ""))


# ---------------------------------------------------------------------------------------
# Payload Cache (pid:...) + Details-Cache
# ---------------------------------------------------------------------------------------

_PAYLOAD_PREFIX = "pid:"
_PAYLOAD_CACHE_SUBDIR = "payload_cache_fav"
_PAYLOAD_TTL_SECONDS = max(3600, _get_setting_int("payload_ttl_seconds", 7 * 24 * 3600))  # 7 Tage
_PAYLOAD_MAX_BYTES = max(50_000, _get_setting_int("payload_max_bytes", 2_000_000))        # 2MB

_DETAILS_CACHE_SUBDIR = "fav_details_cache"
_DETAILS_TTL_SECONDS = max(3600, _get_setting_int("fav_details_ttl_seconds", 3 * 24 * 3600))  # 3 Tage


# Payload-GC nicht bei jedem Öffnen laufen lassen (Performance)
_PAYLOAD_GC_INTERVAL_SECONDS = 6 * 3600  # 6 Stunden
_LAST_PAYLOAD_GC_TS = 0.0


def _vfs_mtime(path: str) -> float:
    """mtime für VFS/Local Pfade (best effort)."""
    try:
        st = xbmcvfs.Stat(path)
        mt = getattr(st, "st_mtime", None)
        if mt is not None:
            return float(mt)
    except Exception:
        pass
    try:
        return float(os.path.getmtime(path))
    except Exception:
        return 0.0



def _safe_mkdirs(path: str) -> None:
    try:
        if path and not xbmcvfs.exists(path):
            xbmcvfs.mkdirs(path)
    except Exception:
        pass


def _payload_cache_dir() -> str:
    try:
        base = common.addon_data_dir
    except Exception:
        base = xbmcvfs.translatePath("special://temp/")
    p = os.path.join(base, _PAYLOAD_CACHE_SUBDIR)
    _safe_mkdirs(p)
    return p


def _payload_path_from_pid(pid: str) -> str:
    pid_clean = (pid or "").strip().replace("/", "").replace("\\", "")
    return os.path.join(_payload_cache_dir(), f"{pid_clean}.json")


def cleanup_payload_cache(ttl_seconds: int = None) -> None:
    """Löscht alte pid-Payloads aus dem Cache (best effort) – throttled."""
    ttl = int(ttl_seconds or _PAYLOAD_TTL_SECONDS)
    if ttl <= 0:
        return

    # Throttle: wenn ohne explizites ttl aufgerufen, nur alle X Stunden laufen lassen
    global _LAST_PAYLOAD_GC_TS
    now_ts = dt_module.datetime.utcnow().timestamp()
    try:
        if ttl_seconds is None and (now_ts - float(_LAST_PAYLOAD_GC_TS or 0.0)) < float(_PAYLOAD_GC_INTERVAL_SECONDS):
            return
        _LAST_PAYLOAD_GC_TS = now_ts
    except Exception:
        pass

    try:
        cache_dir = _payload_cache_dir()
        if not cache_dir or not xbmcvfs.exists(cache_dir):
            return

        _, files = xbmcvfs.listdir(cache_dir)
        for fn in files or []:
            if not fn.lower().endswith(".json"):
                continue
            fp = os.path.join(cache_dir, fn)
            mtime = _vfs_mtime(fp)
            if not mtime:
                continue
            if (now_ts - float(mtime)) > ttl:
                try:
                    xbmcvfs.delete(fp)
                except Exception:
                    pass
    except Exception:
        pass


def _payload_file_is_valid(fp: str) -> bool:
    """Best-effort Prüfung: existiert, nicht leer, JSON parsebar."""
    try:
        if not fp or not xbmcvfs.exists(fp):
            return False
        try:
            st = xbmcvfs.Stat(fp)
            if st and hasattr(st, "st_size") and int(st.st_size()) < 5:
                return False
        except Exception:
            pass

        with xbmcvfs.File(fp, "r") as fh:
            raw = fh.read()

        if raw is None:
            return False
        if isinstance(raw, bytes):
            raw = raw.decode("utf-8", errors="replace")
        raw = (raw or "").lstrip("\ufeff").strip()
        if not raw:
            return False

        obj = json.loads(raw)
        return isinstance(obj, (dict, list))
    except Exception:
        return False


def pack_payload(obj) -> str:
    """
    Speichert obj als JSON in addon_data_dir/.../payload_cache_fav/<pid>.json und gibt 'pid:<pid>' zurück.
    Robust:
      - wenn Datei existiert aber leer/kaputt -> wird neu geschrieben
      - wenn Schreiben/Validieren scheitert -> Fallback: JSON inline zurückgeben
    (Stable: sort_keys=True => bessere Cache-Hitrate)
    """
    try:
        raw = json.dumps(obj, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    except Exception:
        raw = json.dumps({"_payload_error": "json_dumps_failed"}, ensure_ascii=False, sort_keys=True, separators=(",", ":"))

    raw_b = raw.encode("utf-8", errors="replace")
    if len(raw_b) > _PAYLOAD_MAX_BYTES:
        log(f"Payload sehr groß ({len(raw_b)} bytes) – wird trotzdem gecached.", xbmc.LOGWARNING)

    pid = hashlib.sha256(raw_b).hexdigest()[:24]
    fp = _payload_path_from_pid(pid)

    # Wenn schon vorhanden und gültig -> direkt nutzen
    try:
        if _payload_file_is_valid(fp):
            return f"{_PAYLOAD_PREFIX}{pid}"
    except Exception:
        pass

    # Sonst neu schreiben (atomisch, Cache ist nicht kritisch -> ohne heavy fsync)
    try:
        _safe_mkdirs(os.path.dirname(fp))

        try:
            common.atomic_write_bytes(fp, raw_b, backup=False, durable=False)
        except Exception:
            with xbmcvfs.File(fp, "w") as fh:
                fh.write(raw)

        # Validieren – wenn immer noch kaputt, lieber inline zurückgeben (damit Navigation nicht bricht)
        if _payload_file_is_valid(fp):
            return f"{_PAYLOAD_PREFIX}{pid}"

        try:
            if xbmcvfs.exists(fp):
                xbmcvfs.delete(fp)
        except Exception:
            pass
        return raw

    except Exception as e:
        log(f"pack_payload write failed: {e}", xbmc.LOGWARNING)
        return raw



def unpack_payload(data_str):
    """
    Akzeptiert entweder:
      - 'pid:<id>'  -> lädt JSON aus Datei
      - JSON String -> json.loads
    """
    if data_str is None:
        return None

    s = str(data_str)

    candidates = [s]
    try:
        uq = unquote2(s)
        if uq != s:
            candidates.append(uq)
    except Exception:
        pass

    for cand in candidates:
        c = (cand or "").strip()
        if not c:
            continue

        if c.startswith(_PAYLOAD_PREFIX):
            pid = c[len(_PAYLOAD_PREFIX):].strip()
            fp = _payload_path_from_pid(pid)
            try:
                if xbmcvfs.exists(fp):
                    with xbmcvfs.File(fp, "r") as fh:
                        raw = fh.read()
                    if raw is None:
                        return None
                    if isinstance(raw, bytes):
                        raw = raw.decode("utf-8", errors="replace")
                    raw = (raw or "").lstrip("\ufeff").strip()
                    return json.loads(raw) if raw else None
            except Exception as e:
                log(f"unpack_payload pid read failed ({pid}): {e}", xbmc.LOGWARNING)
                return None

        try:
            c = (c or "").lstrip("\ufeff")
            return json.loads(c)
        except Exception:
            continue

    return None


def _details_cache_dir() -> str:
    try:
        base = common.addon_data_dir
    except Exception:
        base = xbmcvfs.translatePath("special://temp/")
    p = os.path.join(base, _DETAILS_CACHE_SUBDIR)
    _safe_mkdirs(p)
    return p


def _details_cache_path(details_url: str) -> str:
    h = hashlib.md5((details_url or "").encode("utf-8", errors="ignore")).hexdigest()
    return os.path.join(_details_cache_dir(), f"{h}.json")


def _load_details_from_cache(details_url: str):
    fp = _details_cache_path(details_url)
    try:
        if not xbmcvfs.exists(fp):
            return None
        try:
            age = dt_module.datetime.utcnow().timestamp() - float(os.path.getmtime(fp))
            if age > _DETAILS_TTL_SECONDS:
                return None
        except Exception:
            pass

        with xbmcvfs.File(fp, "r") as fh:
            raw = fh.read()
        d = json.loads(raw) if raw else None
        return d if isinstance(d, dict) else None
    except Exception:
        return None


def _save_details_to_cache(details_url: str, details_json: dict) -> None:
    fp = _details_cache_path(details_url)
    try:
        with xbmcvfs.File(fp, "w") as fh:
            fh.write(json.dumps(details_json, ensure_ascii=False))
    except Exception:
        pass


def fetch_details_json(details_url: str):
    """
    Lädt details.json:
      1) aus On-Disk Cache (TTL)
      2) sonst via HTTP
    Rückgabe: (dict_or_empty, err_or_None)
    """
    if not details_url:
        return {}, "details_url fehlt"

    cached = _load_details_from_cache(details_url)
    if isinstance(cached, dict) and cached:
        return cached, None

    try:
        req = common.get_authenticated_request(details_url)
        if not req:
            return {}, "Kein authentifizierter Request."
        with _robust_urlopen(req, timeout=NET_TIMEOUT) as response:
            if response.getcode() != 200:
                return {}, f"HTTP {response.getcode()}"
            d = json.loads(response.read().decode("utf-8-sig"))
            if not isinstance(d, dict):
                d = {}
            if d:
                _save_details_to_cache(details_url, d)
            return d, None
    except Exception as e:
        return {}, str(e)


# ---------------------------------------------------------------------------------------
# Init: Cache-Verzeichnis (nur EINMAL)
# ---------------------------------------------------------------------------------------
try:
    if not common.addon_data_dir or not isinstance(common.addon_data_dir, str):
        raise ValueError("common.addon_data_dir ist ungültig oder nicht initialisiert.")
    LOCAL_IMAGE_CACHE_DIR_FAV = os.path.join(common.addon_data_dir, LOCAL_IMAGE_CACHE_SUBDIR_FAV)
    if LOCAL_IMAGE_CACHE_DIR_FAV and not xbmcvfs.exists(LOCAL_IMAGE_CACHE_DIR_FAV):
        xbmcvfs.mkdirs(LOCAL_IMAGE_CACHE_DIR_FAV)
        log(f"Favoriten-Cache-Verzeichnis erstellt: {LOCAL_IMAGE_CACHE_DIR_FAV}")
except Exception as e:
    log(f"FATALER FEHLER beim Initialisieren der Favoriten-Pfade: {e}", xbmc.LOGERROR)
    xbmcgui.Dialog().notification(
        "Addon Fehler",
        "Favoriten-Datenpfade konnten nicht initialisiert werden.",
        xbmcgui.NOTIFICATION_ERROR,
        8000
    )
    LOCAL_IMAGE_CACHE_DIR_FAV = ""


# ---------------------------------------------------------------------------------------
# Datei I/O
# ---------------------------------------------------------------------------------------
def _serialize_json_to_file(obj, dest_path):
    """Schreibt JSON atomisch (best effort)."""
    try:
        # Kompakt schreiben + atomischer Replace (durable, weil Basis-Cache)
        data = json.dumps(obj, ensure_ascii=False, separators=(",", ":"), sort_keys=False).encode("utf-8", errors="replace")
        try:
            common.atomic_write_bytes(dest_path, data, backup=True, durable=True)
            return True
        except Exception:
            # Fallback: VFS write
            with xbmcvfs.File(dest_path, 'w') as fh:
                fh.write(data.decode("utf-8", errors="replace"))
            return True
    except Exception as e:
        log(f"Speichern der Cache-Datei fehlgeschlagen: {dest_path} -> {e}", xbmc.LOGWARNING)
        return False

def _load_json_from_file(src_path):
    try:
        if not xbmcvfs.exists(src_path):
            return None
        with xbmcvfs.File(src_path, 'r') as fh:
            data = fh.read()
        if not data:
            return None
        # BOM robust
        if isinstance(data, str):
            data = data.lstrip("\ufeff")
        return json.loads(data)
    except Exception as e:
        log(f"Lesen der Cache-Datei fehlgeschlagen: {src_path} -> {e}", xbmc.LOGWARNING)
        return None

def _get_seriesfav_cache_path():
    try:
        return os.path.join(common.addon_data_dir, "seriesFav.cache.json")
    except Exception:
        return os.path.join(xbmcvfs.translatePath("special://temp/"), "seriesFav.cache.json")

def _get_seriesfav_remote_meta_path():
    """Meta-Cache für HEAD/Content-Length der Favoritenliste."""
    try:
        return os.path.join(common.addon_data_dir, "seriesFav.remote_meta.json")
    except Exception:
        return os.path.join(xbmcvfs.translatePath("special://temp/"), "seriesFav.remote_meta.json")


def _load_seriesfav_remote_meta():
    meta_path = _get_seriesfav_remote_meta_path()
    meta = _load_json_from_file(meta_path)
    return meta if isinstance(meta, dict) else {}


def _save_seriesfav_remote_meta(meta: dict) -> None:
    try:
        meta_path = _get_seriesfav_remote_meta_path()
        _serialize_json_to_file(meta or {}, meta_path)
    except Exception:
        pass


# ---------------------------------------------------------------------------------------
# Bild-Cache-Funktionen (delegiert an common)
# ---------------------------------------------------------------------------------------

def get_cache_filename(url):
    # kompatibel zu series.py/common.py
    try:
        return common.get_cache_filename(url)
    except Exception:
        # Fallback (sollte praktisch nie passieren)
        try:
            return hashlib.md5((url or "").encode("utf-8", errors="ignore")).hexdigest() + ".jpg"
        except Exception:
            return "invalid_url.jpg"


def get_cached_image(url):
    # lokale Pfade direkt akzeptieren
    if not url or not isinstance(url, str):
        return None
    if not url.startswith("http"):
        return url if xbmcvfs.exists(url) else None
    try:
        return common.get_cached_image(url, cache_dir=LOCAL_IMAGE_CACHE_DIR_FAV)
    except Exception:
        return None


def download_and_cache_image(url, timeout_s=15):
    """Silent download into cache. Returns cached file path or None."""
    if not url or not isinstance(url, str):
        return None
    if not url.startswith("http"):
        return url if xbmcvfs.exists(url) else None
    try:
        return common.download_and_cache_image(url, cache_dir=LOCAL_IMAGE_CACHE_DIR_FAV, timeout_s=timeout_s)
    except Exception:
        return None

# ---------------------------------------------------------------------------------------
# Artwork Helfer (MQ8)
# ---------------------------------------------------------------------------------------
def _force_window_fanart(img_path: str):
    def _ok(s): return isinstance(s, str) and s.strip()
    if not _ok(img_path):
        return
    try:
        if common.addon_handle != -1:
            xbmcplugin.setProperty(common.addon_handle, "fanart_image", img_path)
    except Exception:
        pass
    for win_id in (10025, 10028, 10000):
        try:
            win = xbmcgui.Window(win_id)
            for key in ("fanart_image", "Fanart_Image", "background_fanart"):
                win.setProperty(key, img_path)
        except Exception:
            pass


try:
    _ADDON = xbmcaddon.Addon()
    _ADDON_FANART_PATH = (_ADDON.getAddonInfo("fanart") or "").replace("\\", "/").lower()
except Exception:
    _ADDON = None
    _ADDON_FANART_PATH = ""


def _set_art_mq8(li, *, thumb=None, icon=None, poster=None, fanart=None, tvshow_fanart=None, **extra):
    def _ok(s): return isinstance(s, str) and s.strip()

    _addon_fanart = _ADDON_FANART_PATH

    def _clean(p):
        if not _ok(p):
            return ""
        p2 = p.replace("\\", "/").lower()
        if _addon_fanart and p2 == _addon_fanart:
            return ""
        return p

    thumb = _clean(thumb)
    icon = _clean(icon)
    poster = _clean(poster)
    fanart = _clean(fanart)
    tvshow_fanart = _clean(tvshow_fanart)

    fanart_final = fanart or tvshow_fanart or poster or thumb or icon
    poster_final = poster or thumb or icon

    art = {}

    if _ok(poster_final):
        art["poster"] = poster_final
        art["thumb"] = poster_final
        art["icon"] = poster_final
        art["landscape"] = poster_final
    else:
        fallback = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(fallback):
            fallback = common.addon.getAddonInfo('icon')
        art["icon"] = art["thumb"] = art["poster"] = art["landscape"] = fallback

    if _ok(fanart_final):
        art["fanart"] = fanart_final
        for i in range(1, 10):
            art[f"fanart{i}"] = fanart_final
        art["tvshow.fanart"] = fanart_final
        for i in range(1, 10):
            art[f"tvshow.fanart{i}"] = fanart_final

    for k, v in (extra or {}).items():
        if _ok(v):
            art[k] = v

    if art:
        li.setArt(art)

    if _ok(fanart_final):
        try:
            li.setProperty("fanart_image", fanart_final)
            li.setProperty("Fanart_Image", fanart_final)
        except Exception:
            pass
        _force_window_fanart(fanart_final)


def _apply_fanart_to_listitem(
    li,
    *,
    poster: str = None,
    fanart: str = None,
    tvshow_poster: str = None,
    tvshow_fanart: str = None,
    still: str = None
) -> None:
    def _ok(s): return isinstance(s, str) and s.strip()

    poster_final = None
    for cand in (still, poster, tvshow_poster):
        if _ok(cand):
            poster_final = cand
            break

    fanart_final = None
    for cand in (fanart, still, poster, tvshow_fanart, tvshow_poster):
        if _ok(cand):
            fanart_final = cand
            break

    _set_art_mq8(
        li,
        thumb=poster_final or "",
        icon=poster_final or "",
        poster=poster_final or "",
        fanart=fanart_final or "",
        tvshow_fanart=tvshow_fanart or ""
    )


# ---------------------------------------------------------------------------------------
# Hilfsfunktionen
# ---------------------------------------------------------------------------------------
def _parse_year_from_air_date_string(air_date_str, info_tag_ref, item_label_for_log="Item"):
    if not (air_date_str and isinstance(air_date_str, str) and air_date_str.strip()):
        return False

    did_set_year = False
    try:
        try:
            parsed_date = dt_module.datetime.strptime(air_date_str, "%Y-%m-%d")
            info_tag_ref.setYear(parsed_date.year)
            return True
        except Exception:
            pass

        if len(air_date_str) >= 4 and air_date_str[:4].isdigit():
            info_tag_ref.setYear(int(air_date_str[:4]))
            did_set_year = True
    except Exception:
        pass

    return did_set_year


def _split_and_set_list(info_tag, setter_name, value):
    if not value:
        return
    try:
        items = [x.strip() for x in str(value).split(',') if x.strip()]
        if not items:
            return
        getattr(info_tag, setter_name)(items)
    except Exception:
        pass


# ---------------------------------------------------------------------------------------
# Laden von seriesFav.json (mit lokalem Fallback-Cache)
# ---------------------------------------------------------------------------------------
def _normalize_host_and_basepath():
    """Einheitliche Normalisierung (delegiert an common.normalize_ftp_host_basepath)."""
    ftp_username = _get_setting_str("ftp_username", "").strip()
    ftp_password = _get_setting_str("ftp_password", "").strip()

    try:
        host_final, base_path, scheme = common.normalize_ftp_host_basepath()
        return scheme, host_final, ftp_username, ftp_password, base_path
    except Exception:
        # Fallback (sollte praktisch nie passieren)
        ftp_host_raw = _get_setting_str("ftp_host", "").strip()
        base_path_setting = _get_setting_str("ftp_base_path", "").strip()

        scheme = "https"
        ftp_host_cleaned = ftp_host_raw

        if ftp_host_raw.startswith(("http://", "https://")):
            scheme = ftp_host_raw.split("://", 1)[0].lower()
            ftp_host_cleaned = ftp_host_raw.split("://", 1)[1]

        if "@" in ftp_host_cleaned:
            ftp_host_cleaned = ftp_host_cleaned.split("@", 1)[-1]

        ftp_host_cleaned = ftp_host_cleaned.split("/")[0].split("?")[0].split("#")[0].rstrip(":/ ")

        base_path = "/user/downloads/kodiAddon/"
        try:
            if base_path_setting:
                temp_path = base_path_setting.strip()
                if temp_path and temp_path != "/":
                    base_path = temp_path.rstrip("/") + "/"
        except Exception:
            pass

        if not base_path.startswith("/"):
            base_path = "/" + base_path

        return scheme, ftp_host_cleaned, ftp_username, ftp_password, base_path



def load_favorites_json():
    scheme, ftp_host, ftp_username, ftp_password, base_path = _normalize_host_and_basepath()
    if not ftp_host:
        return None, "FTP/HTTP Host fehlt!"

    if not (ftp_username and ftp_password) and not scheme.startswith("http"):
        return None, "FTP Benutzername/Passwort fehlt!"

    url = f"{scheme}://{ftp_host}{base_path}{SERIES_FAV_JSON_FILENAME}"
    cache_path = _get_seriesfav_cache_path()
    meta = _load_seriesfav_remote_meta()

    if not (ftp_username and ftp_password):
        log("Keine FTP-Zugangsdaten gesetzt – versuche ohne Auth.", xbmc.LOGDEBUG)

    # HEAD/Content-Length Check (wie bei Serien/Filmen), um unnötige Downloads zu sparen
    try:
        if scheme.startswith("http") and xbmcvfs.exists(cache_path):
            req_h = common.get_authenticated_request(url)
            if req_h:
                # method override kompatibel mit älterem urllib in Kodi
                try:
                    req_h.get_method = lambda: "HEAD"
                except Exception:
                    pass
                with _robust_urlopen(req_h, timeout=NET_TIMEOUT) as resp_h:
                    cl = None
                    try:
                        cl_raw = resp_h.headers.get("Content-Length")
                        if cl_raw is not None:
                            cl = int(cl_raw)
                    except Exception:
                        cl = None

                    prev_cl = meta.get("content_length")
                    if cl is not None and prev_cl is not None and int(prev_cl) == int(cl):
                        cached = _load_json_from_file(cache_path)
                        if isinstance(cached, list) and cached:
                            log("Favoritenliste laut HEAD/Content-Length unverändert – verwende lokalen Cache.", xbmc.LOGINFO)
                            return cached, None
    except Exception as e:
        # HEAD darf niemals die Funktion kaputt machen – dann einfach normal weiter
        log(f"HEAD/Content-Length Check fehlgeschlagen: {e}", xbmc.LOGDEBUG)

    log(f"Lade Favoriten-JSON von: {url}", xbmc.LOGDEBUG)
    try:
        req = common.get_authenticated_request(url)
        if not req:
            raise ValueError("Konnte Auth Request nicht erstellen.")
        with _robust_urlopen(req, timeout=NET_TIMEOUT) as response:
            if response.getcode() == 200:
                raw = response.read()
                # BOM robust (utf-8-sig)
                try:
                    text = raw.decode("utf-8-sig")
                except Exception:
                    text = raw.decode("utf-8", errors="replace")
                data = json.loads(text)
                if not isinstance(data, list):
                    data = []

                # Cache + Meta speichern
                _serialize_json_to_file(data, cache_path)
                try:
                    cl = response.headers.get("Content-Length")
                    meta["content_length"] = int(cl) if cl is not None else meta.get("content_length")
                except Exception:
                    pass
                meta["last_ok_utc"] = dt_module.datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
                _save_seriesfav_remote_meta(meta)

                return data, None
            else:
                raise ValueError(f"HTTP Error {response.getcode()}")
    except Exception as e:
        log(f"Netzwerkfehler bei {SERIES_FAV_JSON_FILENAME}: {e}", xbmc.LOGWARNING)
        cached = _load_json_from_file(cache_path)
        if isinstance(cached, list) and cached:
            log(f"Offline: Verwende lokale {os.path.basename(cache_path)}", xbmc.LOGINFO)
            return cached, None
        return None, f"Laden/Parsen von {SERIES_FAV_JSON_FILENAME} fehlgeschlagen: {e}"

# ---------------------------------------------------------------------------------------
# Favoriten-Root anzeigen
# ---------------------------------------------------------------------------------------
def show_favorite_series():
    xbmcplugin.setContent(common.addon_handle, "tvshows")

    # pid-Cache aufräumen (best effort)
    cleanup_payload_cache()

    data, err = load_favorites_json()
    if err:
        xbmcgui.Dialog().notification("Fehler", err, "")
        xbmcplugin.endOfDirectory(common.addon_handle, succeeded=False)
        return

    favorite_series_list = data or []

    # 1) Fehlende Bilder scannen
    missing_images = []
    if LOCAL_IMAGE_CACHE_DIR_FAV:
        for serie in favorite_series_list:
            if not isinstance(serie, dict):
                continue
            serie.pop("action", None)

            for key in ("poster", "backcover"):
                url_val = (serie.get(key) or "").strip()
                if url_val.startswith("http"):
                    local_path = get_cached_image(url_val)
                    if local_path:
                        serie[key] = local_path
                    else:
                        missing_images.append((serie, url_val, key))

    # 2) Fehlende Bilder laden
    if missing_images and LOCAL_IMAGE_CACHE_DIR_FAV:
        total = len(missing_images)
        mon = xbmc.Monitor()

        show_progress = total > 2

        progress = None
        if show_progress:
            progress = xbmcgui.DialogProgress()
            progress.create(f"{common.addon_name} - Bilder", "Lade Favoriten-Bilder...")

        def _dl_tuple(tup):
            serie, image_url, key = tup
            path = download_and_cache_image(image_url)
            return (serie, key, path)

        ok_cnt, fail_cnt = 0, 0

        if PARALLEL_IMAGES and _HAS_FUTURES and IMAGE_WORKERS > 1:
            try:
                with ThreadPoolExecutor(max_workers=IMAGE_WORKERS) as ex:
                    futures = [ex.submit(_dl_tuple, item) for item in missing_images]
                    for i, fut in enumerate(as_completed(futures), 1):
                        if mon.abortRequested() or (progress and progress.iscanceled()):
                            break
                        try:
                            serie, key, path = fut.result()
                            if path:
                                serie[key] = path
                                ok_cnt += 1
                            else:
                                fail_cnt += 1
                        except Exception:
                            fail_cnt += 1

                        if progress:
                            progress.update(int(i * 100 / max(total, 1)),
                                            f"({i}/{total}) Lade Bilder …")
            except Exception as e:
                log(f"Parallel-Downloader Fehler: {e}", xbmc.LOGWARNING)
        else:
            for i, (serie, image_url, key) in enumerate(missing_images, start=1):
                if mon.abortRequested() or (progress and progress.iscanceled()):
                    break
                try:
                    path = download_and_cache_image(image_url)
                    if path:
                        serie[key] = path
                        ok_cnt += 1
                    else:
                        fail_cnt += 1
                except Exception as e:
                    fail_cnt += 1
                    log(f"Imagecache Fehler ({key}) für {serie.get('name')}: {e}", xbmc.LOGDEBUG)

                if progress:
                    progress.update(int(i * 100 / max(total, 1)),
                                    f"({i}/{total}) Lade {key.capitalize()} …")

        if progress:
            progress.close()

        log(f"ImageCache Favoriten: ok={ok_cnt}, fail={fail_cnt}")

    # 3) Sortierung
    sort_method_param = "added"
    if len(sys.argv) > 2 and sys.argv[2].startswith('?'):
        try:
            plugin_params = dict(urllib.parse.parse_qsl(sys.argv[2][1:]))
            sort_method_param = plugin_params.get("sort", sort_method_param).lower()
        except Exception:
            pass

    if sort_method_param in ("date", "added"):
        def parse_date_robust_fav(s_item):
            d_str = s_item.get("added", "")
            for f in ("%Y-%m-%d %H:%M:%S.%f", "%Y-%m-%d %H:%M:%S", "%Y-%m-%d"):
                try:
                    return dt_module.datetime.strptime(d_str, f)
                except Exception:
                    continue
            return dt_module.datetime(1970, 1, 1)
        favorite_series_list = sorted(favorite_series_list, key=parse_date_robust_fav, reverse=True)

    elif sort_method_param == "year":
        def get_year_fav(s_item):
            year_str = str(s_item.get("start_year", "0"))[:4]
            return int(year_str) if year_str.isdigit() else 0
        favorite_series_list = sorted(favorite_series_list, key=get_year_fav, reverse=True)

    else:
        favorite_series_list = sorted(favorite_series_list, key=lambda s: s.get("name", "").lower())

    # 4) Erstes Fanart für Container setzen
    first_fanart = ""
    for s in favorite_series_list:
        cand = (s.get("backcover") or s.get("poster") or "").strip()
        if cand:
            first_fanart = cand
            break
    _force_window_fanart(first_fanart or common.addon.getAddonInfo('icon'))

    # 5) Random-Item oben
    random_item = xbmcgui.ListItem(label="Zufällige Serie starten")
    random_item.setProperty("specialsort", "top")
    vi_r = random_item.getVideoInfoTag()
    vi_r.setPlot("Startet die Wiedergabe einer zufälligen Episode aus deinen Favoriten-Serien.")
    vi_r.setMediaType("video")

    rand_icon = os.path.join(common.addon_path, "random.png")
    series_icon = os.path.join(common.addon_path, "series.png")
    icon_final = rand_icon if xbmcvfs.exists(rand_icon) else (series_icon if xbmcvfs.exists(series_icon) else common.addon.getAddonInfo('icon'))

    _apply_fanart_to_listitem(
        random_item,
        poster=icon_final,
        fanart=first_fanart or icon_final,
        tvshow_poster=icon_final,
        tvshow_fanart=first_fanart or icon_final
    )

    random_url = common.build_url({'action': 'play_random_favorite_series'})
    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_url, listitem=random_item, isFolder=False)

    if not favorite_series_list:
        xbmcgui.Dialog().notification(common.addon_name, "Keine Favoriten gefunden.", "")

    # 6) Serien listen
    for serie_item in favorite_series_list:
        if not isinstance(serie_item, dict):
            continue

        name = (serie_item.get("name") or "Unbekannt").strip()
        li = xbmcgui.ListItem(label=name)

        final_poster = (serie_item.get("poster") or "").strip()
        final_fanart = (serie_item.get("backcover") or "").strip()
        if not final_fanart:
            final_fanart = final_poster

        _apply_fanart_to_listitem(
            li,
            poster=final_poster or final_fanart,
            fanart=final_fanart or final_poster,
            tvshow_poster=final_poster or final_fanart,
            tvshow_fanart=final_fanart or final_poster
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(name)
        info_tag.setPlot(serie_item.get("overview", ""))
        info_tag.setMediaType("tvshow")
        info_tag.setTvShowTitle(name)
        info_tag.setPlaycount(0)

        y_str = str(serie_item.get("start_year", ""))
        year_val, premiered = 0, ""
        if len(y_str) >= 4 and y_str[:4].isdigit():
            year_val = int(y_str[:4])
        if len(y_str) == 10 and y_str[4] == '-' and y_str[7] == '-':
            premiered = y_str
        elif year_val > 0:
            premiered = f"{year_val}-01-01"
        if premiered:
            info_tag.setPremiered(premiered)
        if year_val > 0:
            info_tag.setYear(year_val)

        series_vote_avg_str = serie_item.get("vote_average")
        if series_vote_avg_str is not None:
            try:
                rv = float(str(series_vote_avg_str).replace(",", "."))
                if rv > 0:
                    info_tag.setRating(rv)
            except Exception:
                pass
        else:
            imdb_rating_str = serie_item.get("imdbRating")
            if imdb_rating_str is not None:
                try:
                    rv = float(str(imdb_rating_str).replace(",", "."))
                    if rv > 0:
                        info_tag.setRating(rv)
                except Exception:
                    pass

        # Votes (IMDb bevorzugt, sonst TMDB)
        votes_val = 0
        for imdb_key in ("imdbVotes", "imdb_votes", "imdbvotes", "votes_imdb"):
            raw = serie_item.get(imdb_key)
            if raw:
                try:
                    votes_val = int(re.sub(r"[^\d]", "", str(raw)))
                except Exception:
                    votes_val = 0
                if votes_val > 0:
                    break
        if votes_val <= 0:
            for tmdb_key in ("vote_count", "votes", "tmdb_vote_count"):
                raw = serie_item.get(tmdb_key)
                if raw is not None:
                    try:
                        votes_val = int(str(raw))
                    except Exception:
                        votes_val = 0
                    if votes_val > 0:
                        break
        if votes_val > 0:
            try:
                info_tag.setVotes(votes_val)
            except Exception:
                pass

        fsk_val = serie_item.get("fsk")
        fsk_processed = str(fsk_val).strip() if fsk_val is not None else ""
        if fsk_processed:
            info_tag.setMpaa(f"FSK {fsk_processed}")

        if serie_item.get("added"):
            info_tag.setDateAdded(serie_item.get("added"))

        _split_and_set_list(info_tag, "setGenres", serie_item.get("genre", ""))
        _split_and_set_list(info_tag, "setStudios", serie_item.get("studio", ""))

        details_url_nav = (serie_item.get("details_url") or "").strip()
        if not details_url_nav:
            log(f"Serie ohne details_url ignoriert: {name}", xbmc.LOGDEBUG)
            continue

        nav_data = serie_item.copy()
        nav_data['series_vote_average'] = series_vote_avg_str

        # WICHTIG: data= pack_payload statt json.dumps (kurze URLs)
        target_url = common.build_url({"action": "show_fav_seasons", "data": pack_payload(nav_data)})
        li.setProperty('IsPlayable', 'false')

        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=target_url, listitem=li, isFolder=True)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_DATEADDED)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)

    xbmcplugin.endOfDirectory(common.addon_handle, succeeded=True, cacheToDisc=True)
    log("Favoriten-Verzeichnis erfolgreich aufgebaut.")


# ---------------------------------------------------------------------------------------
# Favoriten-Staffeln anzeigen
# ---------------------------------------------------------------------------------------
def show_fav_seasons(data_json_str):
    xbmcplugin.setContent(common.addon_handle, "seasons")

    try:
        series_info = unpack_payload(data_json_str)
        if not isinstance(series_info, dict):
            raise ValueError("Ungültige Seriendaten.")
        series_name = series_info.get("name", "Unbekannte Serie")
        series_backcover = series_info.get("backcover", "")
        series_poster = series_info.get("poster", "")
        current_series_genre = series_info.get("genre", "")
        current_series_studio = series_info.get("studio", "")
        series_start_year_str = str(series_info.get("start_year", ""))
        current_series_fsk_raw = series_info.get("fsk")
        current_series_overall_vote_average = series_info.get("series_vote_average")
    except Exception as e:
        xbmcgui.Dialog().notification("Fehler", f"Ungültige Seriendaten: {e}", xbmcgui.NOTIFICATION_ERROR)
        log(f"Fehler in show_fav_seasons (Datenparsen): {e}", xbmc.LOGERROR)
        xbmcplugin.endOfDirectory(common.addon_handle, succeeded=False)
        return

    series_fanart_for_win = get_cached_image(series_backcover) or series_backcover or series_poster
    _force_window_fanart(series_fanart_for_win)

    details_url = series_info.get("details_url")
    if not details_url:
        xbmcgui.Dialog().notification("Fehler", "Keine Details-URL gefunden.", xbmcgui.NOTIFICATION_ERROR)
        xbmcplugin.endOfDirectory(common.addon_handle, succeeded=False)
        return

    # Zufall oben
    random_item = xbmcgui.ListItem(label="Zufällige Episode starten")
    random_item.setProperty("specialsort", "top")
    vi = random_item.getVideoInfoTag()
    vi.setPlot(f"Startet die Wiedergabe einer zufälligen Episode aus der Serie '{series_name}'.")
    vi.setMediaType('video')

    icon_r = os.path.join(common.addon_path, "random.png")
    if not xbmcvfs.exists(icon_r):
        icon_r = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(icon_r):
            icon_r = common.addon.getAddonInfo('icon')

    _set_art_mq8(random_item, thumb=icon_r, icon=icon_r, fanart=series_fanart_for_win)

    random_series_url = common.build_url({
    "action": "play_random_fav_episode_from_this_series",
    "data": pack_payload(series_info.copy())
})

    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_series_url, listitem=random_item, isFolder=False)

    # Details laden (mit Cache)
    details_json, err = fetch_details_json(details_url)
    if err:
        xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {err}", xbmcgui.NOTIFICATION_ERROR)
        log(f"Fehler beim Laden von details.json für '{series_name}': {err}", xbmc.LOGERROR)
        details_json = {}

    # FSK ggf. aus Details übernehmen
    fsk_from_seriesfav = str(current_series_fsk_raw).strip() if current_series_fsk_raw is not None else ""
    if not fsk_from_seriesfav and details_json:
        details_series_info_node = details_json.get("series_info", {})
        fsk_from_details_node = details_series_info_node.get("fsk")
        fsk_from_details_processed = str(fsk_from_details_node).strip() if fsk_from_details_node is not None else ""
        if fsk_from_details_processed:
            current_series_fsk_raw = fsk_from_details_node

    season_keys = sorted(
        [k for k in details_json if isinstance(k, str) and k.lower().startswith("season")],
        key=lambda s_key: int(s_key.lower().replace("season", "").strip()) if s_key.lower().replace("season", "").strip().isdigit() else float('inf')
    )

    for season_key in season_keys:
        season_data = details_json.get(season_key, {})
        if not isinstance(season_data, dict):
            continue

        tmdb_season_details = season_data.get("tmdb_season_details", {})
        season_num_str = season_key.lower().replace("season", "").strip()
        season_num = int(season_num_str) if season_num_str.isdigit() else -1
        season_label_name = tmdb_season_details.get("name", f"Staffel {season_num}" if season_num != -1 else season_key)

        episode_count_tmdb = tmdb_season_details.get("episode_count")
        label = f"{season_label_name} ({episode_count_tmdb} Episoden)" if isinstance(episode_count_tmdb, int) and episode_count_tmdb > 0 else season_label_name

        li = xbmcgui.ListItem(label=label)

        season_poster_url = season_data.get("poster", series_poster)
        final_poster_path = get_cached_image(season_poster_url) or season_poster_url

        _set_art_mq8(
            li,
            thumb=final_poster_path,
            icon=final_poster_path,
            poster=final_poster_path,
            fanart=series_fanart_for_win
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setMediaType("season")
        info_tag.setTitle(season_label_name)
        if season_num != -1:
            info_tag.setSeason(season_num)
        info_tag.setTvShowTitle(series_name)
        info_tag.setPlot(tmdb_season_details.get("overview", season_data.get("overview", "")))

        s_air_date_str = str(tmdb_season_details.get("air_date", season_data.get("air_date", "")))
        s_year = 0
        s_premiered = ""
        if s_air_date_str and len(s_air_date_str) >= 4 and s_air_date_str[:4].isdigit():
            s_year = int(s_air_date_str[:4])
        if len(s_air_date_str) == 10:
            s_premiered = s_air_date_str
        elif s_year > 0:
            s_premiered = f"{s_year}-01-01"
        if not s_premiered and series_start_year_str:
            if len(series_start_year_str) == 10:
                s_premiered = series_start_year_str
            elif len(series_start_year_str) >= 4 and series_start_year_str[:4].isdigit():
                s_premiered = f"{int(series_start_year_str[:4])}-01-01"
        if s_premiered:
            info_tag.setPremiered(s_premiered)
        if s_year > 0:
            info_tag.setYear(s_year)
        elif s_air_date_str:
            _parse_year_from_air_date_string(s_air_date_str, info_tag, label)

        determined_season_rating_value = 0.0
        season_specific_vote_average_str = tmdb_season_details.get("vote_average")
        if season_specific_vote_average_str is not None:
            try:
                rating_val = float(str(season_specific_vote_average_str).replace(",", "."))
                if rating_val > 0:
                    determined_season_rating_value = rating_val
            except Exception:
                pass

        if determined_season_rating_value > 0:
            info_tag.setRating(determined_season_rating_value)
        elif current_series_overall_vote_average is not None:
            try:
                series_rating_float = float(str(current_series_overall_vote_average).replace(",", "."))
                if series_rating_float > 0:
                    info_tag.setRating(series_rating_float)
                    determined_season_rating_value = series_rating_float
            except Exception:
                pass

        fsk_processed_value_season = str(current_series_fsk_raw).strip() if current_series_fsk_raw is not None else ""
        if fsk_processed_value_season:
            info_tag.setMpaa(f"FSK {fsk_processed_value_season}")

        _split_and_set_list(info_tag, "setGenres", current_series_genre)
        _split_and_set_list(info_tag, "setStudios", current_series_studio)

        # WICHTIG: NICHT season_data + episodes in data= mitschleppen
        season_nav_data = {
            "details_url": details_url,
            "season_key": season_key,
            "season_number": season_num,

            "series_fanart": series_backcover,
            "series_poster": series_poster,
            "series_name": series_name,
            "series_genre": current_series_genre,
            "series_studio": current_series_studio,

            "fsk_from_series": current_series_fsk_raw,
            "series_vote_average_from_series": current_series_overall_vote_average,
            "season_vote_average_from_season": determined_season_rating_value if determined_season_rating_value > 0 else None
        }

        folder_url = common.build_url({"action": "show_fav_episodes", "data": pack_payload(season_nav_data)})
        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=folder_url, listitem=li, isFolder=True)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_VIDEO_YEAR)
    xbmcplugin.endOfDirectory(common.addon_handle, cacheToDisc=True)


# ---------------------------------------------------------------------------------------
# Episoden anzeigen
# ---------------------------------------------------------------------------------------
def show_fav_episodes(data_json_str):
    xbmcplugin.setContent(common.addon_handle, "episodes")

    try:
        season_info = unpack_payload(data_json_str)
        if not isinstance(season_info, dict):
            raise ValueError("Ungültige Staffeldaten.")
    except Exception as e:
        xbmcgui.Dialog().notification("Fehler", f"Ungültige Staffeldaten: {e}", xbmcgui.NOTIFICATION_ERROR)
        xbmcplugin.endOfDirectory(common.addon_handle, succeeded=False)
        return

    # Legacy: falls season_data noch inline mitgegeben wurde, nutze es.
    season_data_from_nav = season_info.get("season_data") if isinstance(season_info, dict) else None
    episodes = []
    if isinstance(season_data_from_nav, dict):
        episodes = season_data_from_nav.get("episodes", []) or []

    # Neu: über details_url + season_key nachladen
    if not episodes:
        details_url = (season_info or {}).get("details_url")
        season_key = (season_info or {}).get("season_key")
        details_json, derr = fetch_details_json(details_url)
        if derr:
            log(f"show_fav_episodes: Details laden fehlgeschlagen: {derr}", xbmc.LOGWARNING)
            details_json = {}
        season_data_from_nav = details_json.get(season_key, {}) if isinstance(details_json, dict) else {}
        episodes = (season_data_from_nav.get("episodes", []) or []) if isinstance(season_data_from_nav, dict) else []

    if not episodes or not isinstance(episodes, list):
        xbmcgui.Dialog().notification("Info", "Keine Episoden in dieser Staffel gefunden.", "")
        xbmcplugin.endOfDirectory(common.addon_handle)
        return

    series_name_for_ep = season_info.get("series_name", "Unbekannte Serie")
    current_season_number = season_info.get("season_number", -1)
    series_genre_str_for_ep = season_info.get("series_genre", "")
    series_studio_str_for_ep = season_info.get("series_studio", "")
    series_fanart_for_ep = season_info.get("series_fanart", "")
    series_poster_for_ep = season_info.get("series_poster", "")

    series_fsk_raw_for_ep = season_info.get("fsk_from_series")
    series_overall_vote_average_for_ep = season_info.get("series_vote_average_from_series")
    season_vote_average_for_ep = season_info.get("season_vote_average_from_season")

    series_fanart_cached = get_cached_image(series_fanart_for_ep) or series_fanart_for_ep
    series_poster_cached = get_cached_image(series_poster_for_ep) or series_poster_for_ep

    _force_window_fanart(series_fanart_cached or series_poster_cached)

    # Zufall oben
    random_item = xbmcgui.ListItem(label="Zufällige Episode starten")
    random_item.setProperty("specialsort", "top")
    vi = random_item.getVideoInfoTag()
    vi.setPlot(
        f"Startet die Wiedergabe einer zufälligen Episode aus Staffel {current_season_number}."
        if current_season_number != -1 else "Startet die Wiedergabe einer zufälligen Episode aus dieser Staffel."
    )
    vi.setMediaType('video')

    # data_json_str kann pid:... sein -> passt
    random_url = common.build_url({"action": "play_random_favorite_episode_from_season", "data": data_json_str})

    icon_r = os.path.join(common.addon_path, "random.png")
    if not xbmcvfs.exists(icon_r):
        icon_r = os.path.join(common.addon_path, "series.png")
        if not xbmcvfs.exists(icon_r):
            icon_r = common.addon.getAddonInfo('icon')

    _set_art_mq8(random_item, thumb=icon_r, icon=icon_r, fanart=series_fanart_cached or series_poster_cached)
    xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=random_url, listitem=random_item, isFolder=False)

    episodes = sorted(episodes, key=lambda ep_item: int(ep_item.get("episode_number", 0)) if str(ep_item.get("episode_number", "0")).isdigit() else float('inf'))

    for ep in episodes:
        if not isinstance(ep, dict):
            continue

        ep_title = ep.get("name", "Unbekannte Episode")
        ep_number_val = ep.get("episode_number", "")
        label = f"E{ep_number_val}: {ep_title}" if ep_number_val else ep_title
        if current_season_number != -1:
            label = f"S{str(current_season_number).zfill(2)}{label}"

        li = xbmcgui.ListItem(label=label)
        li.setProperty("IsPlayable", "true")

        playable_path = unquote2(ep.get("file_ort", ""))

        still_url = ep.get("still", "")
        final_still_path = get_cached_image(still_url) or still_url if still_url else ""
        final_thumb = final_still_path or series_poster_cached

        _set_art_mq8(
            li,
            thumb=final_thumb,
            icon=final_thumb,
            poster=final_thumb,
            fanart=series_fanart_cached or final_thumb,
            **{
                "landscape": final_thumb,
                "tvshow.poster": series_poster_cached,
                "tvshow.fanart": series_fanart_cached
            }
        )

        info_tag = li.getVideoInfoTag()
        info_tag.setMediaType("episode")
        info_tag.setTitle(ep_title)
        info_tag.setPlot(ep.get("overview", ""))
        info_tag.setTvShowTitle(series_name_for_ep)

        if current_season_number != -1:
            info_tag.setSeason(current_season_number)
        if ep_number_val:
            try:
                info_tag.setEpisode(int(ep_number_val))
            except Exception:
                info_tag.setEpisode(0)

        air_date_str = ep.get("air_date", "")
        if air_date_str and air_date_str.strip():
            info_tag.setPremiered(air_date_str)
            _parse_year_from_air_date_string(air_date_str, info_tag, label)

        episode_runtime_minutes = ep.get("runtime")
        if isinstance(episode_runtime_minutes, int) and episode_runtime_minutes > 0:
            try:
                info_tag.setDuration(episode_runtime_minutes * 60)
            except Exception:
                pass

        final_rating = 0.0
        for cand in (ep.get("rating"), season_vote_average_for_ep, series_overall_vote_average_for_ep):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info_tag.setRating(final_rating)

        fsk_processed_value_ep = str(series_fsk_raw_for_ep).strip() if series_fsk_raw_for_ep is not None else ""
        if fsk_processed_value_ep:
            info_tag.setMpaa(f"FSK {fsk_processed_value_ep}")

        _split_and_set_list(info_tag, "setDirectors", ep.get("directors", ""))
        _split_and_set_list(info_tag, "setGenres", series_genre_str_for_ep)
        _split_and_set_list(info_tag, "setStudios", series_studio_str_for_ep)

        ep_play_url = common.build_url({
            "action": "play_series",
            "serie_key": playable_path, "title": label, "tmdbid": playable_path
        })
        xbmcplugin.addDirectoryItem(handle=common.addon_handle, url=ep_play_url, listitem=li, isFolder=False)

    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_EPISODE)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_LABEL)
    xbmcplugin.addSortMethod(common.addon_handle, xbmcplugin.SORT_METHOD_DATE)
    xbmcplugin.endOfDirectory(common.addon_handle, cacheToDisc=True)


# ---------------------------------------------------------------------------------------
# Zufallsfunktionen
# ---------------------------------------------------------------------------------------
def play_random_episode_from_season(data_json_str):
    try:
        season_info = unpack_payload(data_json_str)
        if not isinstance(season_info, dict):
            raise ValueError("Geparste Daten sind kein Dictionary.")
    except Exception as e:
        xbmc.log(f"[{common.addon_id}] Fehler beim Parsen der Staffeldaten: {e}", xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Ungültige Staffeldaten.", xbmcgui.NOTIFICATION_ERROR)
        return

    # Legacy / neu kombinieren
    season_data = (season_info or {}).get("season_data") if isinstance(season_info, dict) else None
    episodes = (season_data.get("episodes", []) or []) if isinstance(season_data, dict) else []

    if not episodes:
        details_url = (season_info or {}).get("details_url")
        season_key = (season_info or {}).get("season_key")
        details_json, derr = fetch_details_json(details_url)
        if derr:
            xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {derr}", xbmcgui.NOTIFICATION_ERROR)
            return
        season_data = details_json.get(season_key, {}) if isinstance(details_json, dict) else {}
        episodes = (season_data.get("episodes", []) or []) if isinstance(season_data, dict) else []

    if not episodes or not isinstance(episodes, list):
        xbmcgui.Dialog().notification("Fehler", "Keine Episoden in dieser Staffel gefunden.", xbmcgui.NOTIFICATION_ERROR)
        return

    season_no = season_info.get("season_number", -1)
    series_name = season_info.get("series_name", "Unbekannte Serie")
    series_poster = season_info.get("series_poster", "")
    series_fanart = season_info.get("series_fanart", "")
    series_genre = season_info.get("series_genre", "")
    series_studio = season_info.get("series_studio", "")
    fsk_raw = season_info.get("fsk_from_series")
    series_vote_avg = season_info.get("series_vote_average_from_series")
    season_vote_avg = season_info.get("season_vote_average_from_season")

    autoplay_random = _get_setting_bool("autoplay_random", False)

    def _apply_common_tags(li, ep):
        info = li.getVideoInfoTag()
        info.setMediaType("episode")
        info.setTitle(ep.get("name", "Zufällige Episode"))
        info.setTvShowTitle(series_name)
        info.setPlot(ep.get("overview", ""))

        if season_no != -1:
            info.setSeason(season_no)
        ep_no = ep.get("episode_number", 0)
        if ep_no:
            try:
                info.setEpisode(int(ep_no))
            except Exception:
                pass

        air_date = ep.get("air_date", "")
        if air_date:
            info.setPremiered(air_date)
            _parse_year_from_air_date_string(air_date, info, ep.get("name", ""))

        runtime_min = ep.get("runtime")
        if isinstance(runtime_min, int) and runtime_min > 0:
            try:
                info.setDuration(runtime_min * 60)
            except Exception:
                pass

        final_rating = 0.0
        for cand in (ep.get("rating"), season_vote_avg, series_vote_avg):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info.setRating(final_rating)

        if fsk_raw is not None:
            fsk_s = str(fsk_raw).strip()
            if fsk_s:
                info.setMpaa(f"FSK {fsk_s}")

        _split_and_set_list(info, "setDirectors", ep.get("directors", ""))
        _split_and_set_list(info, "setGenres", series_genre)
        _split_and_set_list(info, "setStudios", series_studio)

    def _apply_art(li, still_url):
        still_cached = get_cached_image(still_url) or still_url
        _set_art_mq8(
            li,
            thumb=still_cached,
            icon=still_cached,
            poster=get_cached_image(series_poster) or series_poster,
            fanart=get_cached_image(series_fanart) or series_fanart
        )

    if autoplay_random:
        playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
        playlist.clear()

        chosen = random.sample(episodes, min(10, len(episodes)))
        added_paths = []

        for ep in chosen:
            path = unquote2(ep.get("file_ort", ""))
            title = ep.get("name")
            if not (path and title):
                continue
            li = xbmcgui.ListItem(path=path)
            li.setProperty("IsPlayable", "true")
            _apply_common_tags(li, ep)
            _apply_art(li, ep.get("still", ""))
            playlist.add(path, li)
            added_paths.append(path)

        if playlist.size() > 0:
            xbmc.Player().play(playlist)

            def _playlist_watcher(expected_paths):
                try:
                    mon = xbmc.Monitor()
                    player = xbmc.Player()

                    expected_norm = [unquote2(p) for p in expected_paths if p]
                    started_for = set()

                    while not mon.abortRequested():
                        if player.isPlayingVideo():
                            try:
                                current_file = unquote2(player.getPlayingFile() or "")
                            except Exception:
                                current_file = ""

                            if current_file:
                                for p_raw, p_norm in zip(expected_paths, expected_norm):
                                    if p_norm == current_file and p_raw not in started_for:
                                        start_series_monitor(p_raw)
                                        started_for.add(p_raw)
                                        break

                        if not player.isPlayingVideo() and not xbmc.getCondVisibility("Player.HasMedia"):
                            break

                        xbmc.sleep(1000)
                except Exception as e:
                    log(f"Playlist-Watcher Fehler: {e}", xbmc.LOGDEBUG)

            try:
                import threading
                t_mon = threading.Thread(target=_playlist_watcher, args=(added_paths,))
                t_mon.daemon = True
                t_mon.start()
            except Exception as e:
                log(f"Playlist-Watcher Startfehler: {e}", xbmc.LOGDEBUG)

        else:
            xbmcgui.Dialog().notification("Zufall", "Keine passende Episode gefunden.", xbmcgui.NOTIFICATION_INFO)

    else:
        ep = random.choice(episodes)
        path = unquote2(ep.get("file_ort", ""))
        title = ep.get("name")
        if not (path and title):
            xbmcgui.Dialog().notification("Zufall", "Unvollständige Episodendaten.", xbmcgui.NOTIFICATION_WARNING)
            return
        li = xbmcgui.ListItem(path=path)
        li.setProperty("IsPlayable", "true")
        _apply_common_tags(li, ep)
        _apply_art(li, ep.get("still", ""))
        xbmc.Player().play(item=path, listitem=li)

        start_series_monitor(path)


def play_random_episode_from_specific_series(series_info_json_str):
    try:
        series_info = unpack_payload(series_info_json_str)
        if not isinstance(series_info, dict):
            raise ValueError("Ungültige Seriendaten für spezifische Zufallswiedergabe.")

        series_name = series_info.get("name", "Unbekannte Serie")
        details_url = series_info.get("details_url")
        series_fsk_raw = series_info.get("fsk")

        # kompatibel: manchmal kommt series_vote_average statt vote_average
        series_overall_vote_avg = series_info.get("vote_average")
        if series_overall_vote_avg is None:
            series_overall_vote_avg = series_info.get("series_vote_average")

        series_poster_for_ep = series_info.get("poster")
        series_fanart_for_ep = series_info.get("backcover")
        series_genre_for_ep = series_info.get("genre", "")
        series_studio_for_ep = series_info.get("studio", "")

        if not details_url:
            xbmcgui.Dialog().notification("Fehler", f"Keine Details-URL für Serie '{series_name}' gefunden.", xbmcgui.NOTIFICATION_ERROR)
            return

        details_json, derr = fetch_details_json(details_url)
        if derr:
            xbmcgui.Dialog().notification("Fehler", f"Details laden fehlgeschlagen: {derr}", xbmcgui.NOTIFICATION_ERROR)
            return

        all_episodes = []
        for season_key, season_data_val in details_json.items():
            if isinstance(season_data_val, dict) and season_key.lower().startswith("season"):
                s_num = 0
                try:
                    s_num = int(re.search(r'(\d+)', season_key).group(1))
                except Exception:
                    pass
                tmdb_s_details = season_data_val.get("tmdb_season_details", {})
                season_vote_avg = tmdb_s_details.get("vote_average")

                for ep_data_item in season_data_val.get("episodes", []):
                    if isinstance(ep_data_item, dict):
                        ep_copy = ep_data_item.copy()
                        ep_copy['parsed_season_number'] = s_num
                        ep_copy['current_season_vote_average'] = season_vote_avg
                        all_episodes.append(ep_copy)

        if not all_episodes:
            xbmcgui.Dialog().notification("Info", f"Keine Episoden in Serie '{series_name}' gefunden.", xbmcgui.NOTIFICATION_INFO)
            return

        random_episode = random.choice(all_episodes)

        playable_path = unquote2(random_episode.get("file_ort"))
        episode_title = random_episode.get("name", "Zufällige Episode")
        if not playable_path:
            xbmcgui.Dialog().notification("Fehler", "Kein Abspielpfad gefunden.", xbmcgui.NOTIFICATION_WARNING)
            return

        li = xbmcgui.ListItem(path=playable_path)
        li.setProperty("IsPlayable", "true")
        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(episode_title)
        info_tag.setTvShowTitle(series_name)
        info_tag.setMediaType("episode")
        info_tag.setPlot(random_episode.get("overview", ""))

        ep_season_num = random_episode.get('parsed_season_number', 0)
        if ep_season_num > 0:
            info_tag.setSeason(ep_season_num)

        ep_num_val = random_episode.get("episode_number", 0)
        if ep_num_val:
            try:
                info_tag.setEpisode(int(ep_num_val))
            except Exception:
                pass

        ep_air_date = random_episode.get("air_date", "")
        if ep_air_date:
            info_tag.setPremiered(ep_air_date)
            _parse_year_from_air_date_string(ep_air_date, info_tag, episode_title)

        ep_runtime = random_episode.get("runtime")
        if isinstance(ep_runtime, int) and ep_runtime > 0:
            try:
                info_tag.setDuration(ep_runtime * 60)
            except Exception:
                pass

        final_ep_rating = 0.0
        for cand in (random_episode.get("rating"),
                     random_episode.get("current_season_vote_average"),
                     series_overall_vote_avg):
            if cand is not None:
                try:
                    final_ep_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_ep_rating > 0:
                break
        if final_ep_rating > 0:
            info_tag.setRating(final_ep_rating)

        fsk_processed = str(series_fsk_raw).strip() if series_fsk_raw is not None else ""
        if fsk_processed:
            info_tag.setMpaa(f"FSK {fsk_processed}")

        _split_and_set_list(info_tag, "setGenres", series_genre_for_ep)
        _split_and_set_list(info_tag, "setStudios", series_studio_for_ep)
        _split_and_set_list(info_tag, "setDirectors", random_episode.get("directors", ""))

        still_url = random_episode.get("still", "")
        still_cached = get_cached_image(still_url) or still_url

        _set_art_mq8(
            li,
            thumb=still_cached,
            icon=still_cached,
            poster=get_cached_image(series_poster_for_ep) or series_poster_for_ep,
            fanart=get_cached_image(series_fanart_for_ep) or series_fanart_for_ep
        )

        xbmc.Player().play(item=playable_path, listitem=li)

        start_series_monitor(playable_path)

    except Exception as e:
        log(f"Fehler in play_random_episode_from_specific_series: {e}\n{traceback.format_exc()}",
            xbmc.LOGERROR)
        xbmcgui.Dialog().notification("Fehler", "Zufallsepisode konnte nicht gestartet werden.", xbmcgui.NOTIFICATION_ERROR)


def find_random_episode_info(count=1, max_retries=20):
    log(f"BG: Suche zufällige Episode(n) (Anzahl: {count})")
    all_series_data, err = load_favorites_json()
    if err:
        log(f"find_random_episode_info: load_favorites_json Fehler: {err}", xbmc.LOGWARNING)
        return None
    if not all_series_data:
        log("find_random_episode_info: keine Favoriten vorhanden.", xbmc.LOGINFO)
        return None

    results = []
    attempts = 0
    max_total_attempts = count * max_retries
    available_series_indices = list(range(len(all_series_data)))

    # In-Memory Details Cache (zusätzlich zum Disk-Cache)
    processed_series_details_cache = {}

    while len(results) < count and attempts < max_total_attempts and available_series_indices:
        attempts += 1
        try:
            chosen_idx = random.choice(available_series_indices)
            random_serie = all_series_data[chosen_idx].copy()
            random_serie.pop("action", None)

            series_title_fav = random_serie.get("name", "Unbekannte Serie")
            details_url = random_serie.get("details_url")
            if not details_url:
                available_series_indices.remove(chosen_idx)
                continue

            details_json = processed_series_details_cache.get(details_url)
            if details_json is None:
                d, derr = fetch_details_json(details_url)
                if derr:
                    processed_series_details_cache[details_url] = False
                    available_series_indices.remove(chosen_idx)
                    continue
                details_json = d if isinstance(d, dict) else {}
                processed_series_details_cache[details_url] = details_json
            elif details_json is False:
                available_series_indices.remove(chosen_idx)
                continue

            all_episodes = []
            for season_key, season_data_val in details_json.items():
                if isinstance(season_data_val, dict) and season_key.lower().startswith("season"):
                    s_num = 0
                    try:
                        s_num = int(re.search(r'(\d+)', season_key).group(1))
                    except Exception:
                        pass

                    tmdb_s_details = season_data_val.get("tmdb_season_details", {})
                    season_vote_avg_for_this_season = tmdb_s_details.get("vote_average")

                    for ep_data_item in season_data_val.get("episodes", []):
                        if isinstance(ep_data_item, dict):
                            ep_copy = ep_data_item.copy()
                            ep_copy['parsed_season_number'] = s_num
                            ep_copy['current_season_vote_average'] = season_vote_avg_for_this_season
                            all_episodes.append(ep_copy)

            if not all_episodes:
                available_series_indices.remove(chosen_idx)
                continue

            random_episode_data = random.choice(all_episodes)

            info = {
                "playable_path": random_episode_data.get("file_ort"),
                "episode_title": random_episode_data.get("name"),
                "overview": random_episode_data.get("overview", ""),
                "air_date": random_episode_data.get("air_date", ""),
                "still": random_episode_data.get("still", ""),
                "series_title": series_title_fav,
                "series_poster": random_serie.get("poster", ""),
                "series_fanart": random_serie.get("backcover", ""),
                "season_number": random_episode_data.get('parsed_season_number', 0),
                "episode_number": random_episode_data.get("episode_number", 0),
                "genre": random_serie.get("genre", ""),
                "studio": random_serie.get("studio", ""),
                "directors": random_episode_data.get("directors", ""),
                "runtime": random_episode_data.get("runtime"),
                "fsk": random_serie.get("fsk"),
                "episode_rating": random_episode_data.get("rating"),
                "season_vote_average": random_episode_data.get("current_season_vote_average"),
                "series_vote_average": random_serie.get("vote_average")
            }
            if info["playable_path"] and info["episode_title"] and not any(item.get("playable_path") == info["playable_path"] for item in results):
                results.append(info)

        except Exception as e:
            log(f"Fehler im Zufallsversuch ({attempts}/{max_total_attempts}): {e}", xbmc.LOGDEBUG)
            try:
                if chosen_idx in available_series_indices:
                    available_series_indices.remove(chosen_idx)
            except Exception:
                pass
            continue

    if not results:
        return None
    return results[0] if count == 1 else results


def play_random_favorite_series():
    autoplay_random = _get_setting_bool("autoplay_random", False)

    if autoplay_random:
        episodes_info_list = find_random_episode_info(count=10)
        if episodes_info_list and isinstance(episodes_info_list, list):
            playlist = xbmc.PlayList(xbmc.PLAYLIST_VIDEO)
            playlist.clear()

            added_paths = []

            for episode_info in episodes_info_list:
                playable_path = episode_info.get("playable_path")
                if not playable_path:
                    continue

                episode_title = episode_info.get("episode_title", "Zufällige Episode")
                li = xbmcgui.ListItem(path=playable_path)
                li.setProperty("IsPlayable", "true")
                info_tag = li.getVideoInfoTag()
                info_tag.setTitle(episode_title)
                info_tag.setTvShowTitle(episode_info.get("series_title", ""))
                info_tag.setMediaType("episode")
                info_tag.setPlot(episode_info.get("overview", ""))

                if episode_info.get("season_number"):
                    info_tag.setSeason(episode_info["season_number"])
                if episode_info.get("episode_number"):
                    info_tag.setEpisode(episode_info["episode_number"])

                runtime_min = episode_info.get("runtime")
                if isinstance(runtime_min, int) and runtime_min > 0:
                    try:
                        info_tag.setDuration(runtime_min * 60)
                    except Exception:
                        pass

                air_date_val = episode_info.get("air_date")
                if air_date_val:
                    info_tag.setPremiered(air_date_val)
                    _parse_year_from_air_date_string(air_date_val, info_tag, episode_title)

                final_rating = 0.0
                for cand in (episode_info.get("episode_rating"),
                             episode_info.get("season_vote_average"),
                             episode_info.get("series_vote_average")):
                    if cand is not None:
                        try:
                            final_rating = float(str(cand).replace(",", "."))
                        except Exception:
                            pass
                    if final_rating > 0:
                        break
                if final_rating > 0:
                    info_tag.setRating(final_rating)

                fsk_raw = episode_info.get("fsk")
                if fsk_raw is not None:
                    fsk_s = str(fsk_raw).strip()
                    if fsk_s:
                        info_tag.setMpaa(f"FSK {fsk_s}")

                _split_and_set_list(info_tag, "setDirectors", episode_info.get("directors", ""))
                _split_and_set_list(info_tag, "setGenres", episode_info.get("genre", ""))
                _split_and_set_list(info_tag, "setStudios", episode_info.get("studio", ""))

                still = episode_info.get("still", "")
                still_cached = get_cached_image(still) or still
                poster_cached = get_cached_image(episode_info.get("series_poster")) or episode_info.get("series_poster")
                fanart_cached = get_cached_image(episode_info.get("series_fanart")) or episode_info.get("series_fanart")

                _set_art_mq8(li, thumb=still_cached, icon=still_cached, poster=poster_cached, fanart=fanart_cached)

                playlist.add(playable_path, li)
                added_paths.append(playable_path)

            if playlist.size() > 0:
                xbmc.Player().play(playlist)

                def _playlist_watcher(expected_paths):
                    try:
                        mon = xbmc.Monitor()
                        player = xbmc.Player()

                        expected_norm = [unquote2(p) for p in expected_paths if p]
                        started_for = set()

                        while not mon.abortRequested():
                            if player.isPlayingVideo():
                                try:
                                    current_file = unquote2(player.getPlayingFile() or "")
                                except Exception:
                                    current_file = ""

                                if current_file:
                                    for p_raw, p_norm in zip(expected_paths, expected_norm):
                                        if p_norm == current_file and p_raw not in started_for:
                                            start_series_monitor(p_raw)
                                            started_for.add(p_raw)
                                            break

                            if not player.isPlayingVideo() and not xbmc.getCondVisibility("Player.HasMedia"):
                                break

                            xbmc.sleep(1000)
                    except Exception as e:
                        log(f"Playlist-Watcher Fehler: {e}", xbmc.LOGDEBUG)

                try:
                    import threading
                    t_mon = threading.Thread(target=_playlist_watcher, args=(added_paths,))
                    t_mon.daemon = True
                    t_mon.start()
                except Exception as e:
                    log(f"Playlist-Watcher Startfehler: {e}", xbmc.LOGDEBUG)

            else:
                xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)
        else:
            xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)

    else:
        episode_info = find_random_episode_info(count=1)
        if not (episode_info and isinstance(episode_info, dict)):
            xbmcgui.Dialog().notification("Zufall", "Keine passende globale Favoriten-Episode gefunden.", xbmcgui.NOTIFICATION_INFO)
            return

        playable_path = episode_info.get("playable_path")
        if not playable_path:
            xbmcgui.Dialog().notification("Zufall", "Globale Episode gefunden, aber kein Pfad.", xbmcgui.NOTIFICATION_WARNING)
            return

        episode_title = episode_info.get("episode_title", "Zufällige Episode")
        li = xbmcgui.ListItem(path=playable_path)
        li.setProperty("IsPlayable", "true")
        info_tag = li.getVideoInfoTag()
        info_tag.setTitle(episode_title)
        info_tag.setTvShowTitle(episode_info.get("series_title", ""))
        info_tag.setMediaType("episode")
        info_tag.setPlot(episode_info.get("overview", ""))

        if episode_info.get("season_number"):
            info_tag.setSeason(episode_info["season_number"])
        if episode_info.get("episode_number"):
            info_tag.setEpisode(episode_info["episode_number"])

        runtime_min = episode_info.get("runtime")
        if isinstance(runtime_min, int) and runtime_min > 0:
            try:
                info_tag.setDuration(runtime_min * 60)
            except Exception:
                pass

        air_date_val = episode_info.get("air_date")
        if air_date_val:
            info_tag.setPremiered(air_date_val)
            _parse_year_from_air_date_string(air_date_val, info_tag, episode_title)

        final_rating = 0.0
        for cand in (episode_info.get("episode_rating"),
                     episode_info.get("season_vote_average"),
                     episode_info.get("series_vote_average")):
            if cand is not None:
                try:
                    final_rating = float(str(cand).replace(",", "."))
                except Exception:
                    pass
            if final_rating > 0:
                break
        if final_rating > 0:
            info_tag.setRating(final_rating)

        fsk_raw = episode_info.get("fsk")
        if fsk_raw is not None:
            fsk_s = str(fsk_raw).strip()
            if fsk_s:
                info_tag.setMpaa(f"FSK {fsk_s}")

        _split_and_set_list(info_tag, "setDirectors", episode_info.get("directors", ""))
        _split_and_set_list(info_tag, "setGenres", episode_info.get("genre", ""))
        _split_and_set_list(info_tag, "setStudios", episode_info.get("studio", ""))

        still = episode_info.get("still", "")
        still_cached = get_cached_image(still) or still
        poster_cached = get_cached_image(episode_info.get("series_poster")) or episode_info.get("series_poster")
        fanart_cached = get_cached_image(episode_info.get("series_fanart")) or episode_info.get("series_fanart")

        _set_art_mq8(li, thumb=still_cached, icon=still_cached, poster=poster_cached, fanart=fanart_cached)
        xbmc.Player().play(item=playable_path, listitem=li)

        start_series_monitor(playable_path)


# ---------------------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------------------
# ---------------------------------------------------------------------------------------
# Router
# ---------------------------------------------------------------------------------------
if __name__ == '__main__':

    # params robust
    try:
        params = dict(urllib.parse.parse_qsl(sys.argv[2][1:], keep_blank_values=True)) \
            if len(sys.argv) > 2 and sys.argv[2].startswith('?') else {}
    except Exception:
        params = {}

    action = (params.get('action') or '').strip() or None

    def _pick_first(keys):
        for k in keys:
            v = params.get(k)
            if v not in (None, ""):
                return v
        return None

    def _get_data(keys):
        v = _pick_first(keys)
        if not v:
            return None
        try:
            return unquote2(v)   # doppelt unquote
        except Exception:
            return v

    # --- Dispatch ---
    if action is None:
        show_favorite_series()

    elif action == 'show_fav_seasons':
        data = _get_data(('data', 'serie_data', 'series_data'))
        if data:
            show_fav_seasons(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Seriendaten für Favoriten-Staffelansicht.", "")

    elif action == 'show_fav_episodes':
        data = _get_data(('data', 'season_data', 'staffel_data'))
        if data:
            show_fav_episodes(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Staffeldaten für Favoriten-Episodenansicht.", "")

    elif action == 'play_random_favorite_series':
        play_random_favorite_series()

    elif action == 'play_random_favorite_episode_from_season':
        data = _get_data(('data', 'season_data', 'staffel_data'))
        if data:
            play_random_episode_from_season(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Staffeldaten für Zufallsepisode aus Favoriten-Staffel.", "")

    # Random Episode aus DIESER Serie (Favoriten-Staffelansicht oben)
    # - alter Name: play_random_episode_from_this_series
    # - neue (empfohlene) konfliktfreie Namen: play_random_favorite_episode_from_this_series / play_random_fav_episode_from_this_series
    elif action in (
        'play_random_episode_from_this_series',
        'play_random_favorite_episode_from_this_series',
        'play_random_fav_episode_from_this_series'
    ):
        data = _get_data(('data', 'serie_data', 'series_data'))
        if data:
            play_random_episode_from_specific_series(data)
        else:
            xbmcgui.Dialog().notification("Fehler", "Keine Seriendaten für Zufallsepisode aus dieser Serie.", "")

    else:
        log(f"Unbekannte Aktion: {action}", xbmc.LOGWARNING)
        show_favorite_series()