# -*- coding: utf-8 -*-
import wx
import os
import sys
import subprocess
import shutil
import urllib.request
import zipfile
import io
import threading
import webbrowser
import json

# yt-dlp installieren
try:
    import yt_dlp as ytdl
except ImportError:
    subprocess.check_call([sys.executable, "-m", "pip", "install", "yt-dlp"])
    import yt_dlp as ytdl

# ------------------------------------------------------------------ #
#  Pfade                                                              #
# ------------------------------------------------------------------ #
if getattr(sys, 'frozen', False):
    BASE_DIR = os.path.dirname(sys.executable)
    ROOT_DIR = os.path.dirname(BASE_DIR)
else:
    BASE_DIR = os.path.dirname(os.path.abspath(__file__))
    ROOT_DIR = os.path.dirname(BASE_DIR)

FFMPEG_DIR  = os.path.join(ROOT_DIR, "ffmpeg_bin")
FFMPEG_FLAG = os.path.join(FFMPEG_DIR, ".ffmpeg_ready")

DEFAULT_DOWNLOAD_DIR = os.path.join(ROOT_DIR, "downloads")
os.makedirs(DEFAULT_DOWNLOAD_DIR, exist_ok=True)

CONFIG_FILE = os.path.join(ROOT_DIR, "AT-config.ini")


def load_download_dir():
    if os.path.isfile(CONFIG_FILE):
        try:
            import configparser
            cfg = configparser.ConfigParser()
            cfg.read(CONFIG_FILE, encoding="utf-8")
            folder = cfg.get("Settings", "download_dir", fallback="")
            if folder and os.path.isdir(folder):
                return folder
        except Exception:
            pass
    return DEFAULT_DOWNLOAD_DIR


def save_download_dir(folder: str):
    try:
        import configparser
        cfg = configparser.ConfigParser()
        cfg["Settings"] = {"download_dir": folder}
        with open(CONFIG_FILE, "w", encoding="utf-8") as f:
            cfg.write(f)
    except Exception:
        pass


def _find_ffmpeg_in_dir():
    for root_dir, dirs, files in os.walk(FFMPEG_DIR):
        if "ffmpeg.exe" in files:
            if root_dir not in os.environ.get("PATH", ""):
                os.environ["PATH"] += os.pathsep + root_dir
            return True
    return False


def ensure_ffmpeg():
    if shutil.which("ffmpeg"):
        return
    if os.path.isfile(FFMPEG_FLAG):
        _find_ffmpeg_in_dir()
        if shutil.which("ffmpeg"):
            return
    os.makedirs(FFMPEG_DIR, exist_ok=True)
    # url = "https://www.gyan.dev/ffmpeg/builds/ffmpeg-release-essentials.zip"
    url = "https://www.ilja-dani.de/I-Tools/FFMPEG.zip"
    dlg = wx.ProgressDialog("FFmpeg einrichten",
                             "FFmpeg wird heruntergeladen (nur einmal nötig)...",
                             maximum=100, style=wx.PD_APP_MODAL | wx.PD_AUTO_HIDE)
    dlg.Pulse()
    try:
        with urllib.request.urlopen(url) as resp:
            data = resp.read()
        dlg.Update(60, "Entpacke FFmpeg...")
        z = zipfile.ZipFile(io.BytesIO(data))
        z.extractall(FFMPEG_DIR)
        dlg.Update(90, "Konfiguriere Pfad...")
        _find_ffmpeg_in_dir()
        open(FFMPEG_FLAG, "w").close()
        dlg.Update(100)
    finally:
        dlg.Destroy()


# ------------------------------------------------------------------ #
#  ARD Audiothek GraphQL API                                          #
# ------------------------------------------------------------------ #
API = "https://api.ardaudiothek.de/graphql"
HEADERS = {"Content-Type": "application/json",
           "User-Agent": "Mozilla/5.0 ARDAudiothekDownloader/3.0"}

# ── Schritt 1: Suche → liefert Items UND ProgramSets ────────────────
SEARCH_Q = """
query Search($q: String!) {
  search(query: $q) {
    items {
      nodes {
        id title publishDate duration synopsis sharingUrl
        image { url }
        audios { url downloadUrl allowDownload mimeType }
        programSet {
          title sharingUrl
          publicationService { title organizationName }
        }
      }
    }
    programSets {
      nodes {
        id title sharingUrl
        publicationService { title organizationName }
      }
    }
  }
}
"""

# ── Schritt 2: Alle Episoden eines ProgramSets laden ─────────────────
EPISODES_Q = """
query Episodes($id: ID!, $count: Int!) {
  programSet(id: $id) {
    title
    sharingUrl
    publicationService { title organizationName }
    items(
      first: $count
      orderBy: PUBLISH_DATE_DESC
      filter: { isPublished: { equalTo: true } }
    ) {
      nodes {
        id title publishDate duration synopsis sharingUrl
        audios { url downloadUrl allowDownload mimeType }
      }
    }
  }
}
"""


def _gql(query: str, variables: dict) -> dict:
    payload = json.dumps({"query": query, "variables": variables}).encode()
    req = urllib.request.Request(API, data=payload, headers=HEADERS)
    with urllib.request.urlopen(req, timeout=20) as r:
        result = json.loads(r.read().decode())
    if "errors" in result:
        raise RuntimeError(result["errors"][0].get("message", str(result["errors"])))
    return result.get("data", {})


def _node_to_entry(node: dict, show_title: str, station: str) -> dict:
    audios   = node.get("audios") or []
    dl_url   = next((a["downloadUrl"] for a in audios if a.get("downloadUrl")), "")
    play_url = next((a["url"]         for a in audios if a.get("url")),         "")
    if not dl_url:
        dl_url = play_url
    raw_date = (node.get("publishDate") or "")
    return {
        "title":        node.get("title", ""),
        "show":         show_title,
        "station":      station,
        "duration":     node.get("duration", 0) or 0,
        "summary":      node.get("synopsis", ""),
        "publish_date": raw_date[:10],
        "url":          dl_url,
        "play_url":     play_url,
        "website":      node.get("sharingUrl", ""),
    }


def search_audiothek(query: str, max_results: int = 50) -> list:
    """
    Zwei-Stufen-Suche:
    1. search() → direkte Item-Treffer + passende Sendungen (ProgramSets)
    2. Für jede gefundene Sendung: Episoden direkt laden (bis max_results gesamt)
    """
    data = _gql(SEARCH_Q, {"q": query})
    search = data.get("search", {})

    # ── Direkte Item-Treffer ──────────────────────────────────────────
    entries = []
    seen_ids = set()
    for node in (search.get("items", {}).get("nodes") or []):
        ps  = node.get("programSet") or {}
        pub = ps.get("publicationService") or {}
        e   = _node_to_entry(
            node,
            ps.get("title", ""),
            pub.get("organizationName") or pub.get("title", "")
        )
        if node["id"] not in seen_ids:
            seen_ids.add(node["id"])
            entries.append(e)

    # ── Sendungen → Episoden nachladen ────────────────────────────────
    ps_nodes = search.get("programSets", {}).get("nodes") or []
    per_show = max(10, (max_results - len(entries)) // max(len(ps_nodes), 1))

    for ps_node in ps_nodes:
        if len(entries) >= max_results:
            break
        ps_id = ps_node.get("id")
        if not ps_id:
            continue
        try:
            ps_data = _gql(EPISODES_Q, {"id": ps_id, "count": per_show})
        except Exception:
            continue

        ps_obj  = ps_data.get("programSet") or {}
        pub     = ps_obj.get("publicationService") or {}
        show_t  = ps_obj.get("title", ps_node.get("title", ""))
        station = pub.get("organizationName") or pub.get("title", "")

        for node in (ps_obj.get("items", {}).get("nodes") or []):
            if len(entries) >= max_results:
                break
            if node["id"] not in seen_ids:
                seen_ids.add(node["id"])
                entries.append(_node_to_entry(node, show_t, station))

    return entries[:max_results]


def fmt_duration(secs):
    if not secs:
        return ""
    m, s = divmod(int(secs), 60)
    h, m = divmod(m, 60)
    return f"{h}:{m:02}:{s:02}" if h else f"{m}:{s:02}"


def sanitize_filename(name: str) -> str:
    for ch in r'\/:*?"<>|':
        name = name.replace(ch, "_")
    return name.strip()


# ------------------------------------------------------------------ #
#  Farb-Theme                                                         #
# ------------------------------------------------------------------ #
BG      = "#0f0f13"
PANEL   = "#1a1a24"
ACCENT  = "#005fa3"
ACCENT2 = "#4da6e0"
TEXT    = "#f0ede8"
SUBTEXT = "#888899"
ITEM_BG = "#22222e"


# ------------------------------------------------------------------ #
#  Ergebnisliste                                                      #
# ------------------------------------------------------------------ #
class AudiothekResultList(wx.ListCtrl):
    def __init__(self, parent):
        super().__init__(parent,
                         style=wx.LC_REPORT | wx.LC_SINGLE_SEL | wx.BORDER_NONE | wx.LC_HRULES)
        self.SetBackgroundColour(wx.Colour(ITEM_BG))
        self.SetForegroundColour(wx.Colour(TEXT))
        self.InsertColumn(0, "Nr.",     width=45)
        self.InsertColumn(1, "Sender",  width=90)
        self.InsertColumn(2, "Sendung", width=185)
        self.InsertColumn(3, "Titel",   width=310)
        self.InsertColumn(4, "Datum",   width=90)
        self.InsertColumn(5, "Länge",   width=70)
        self.InsertColumn(6, "DL",      width=35)
        self.InsertColumn(7, "Status",  width=100)
        self.EnableCheckBoxes(True)
        self._entries = []
        self.Bind(wx.EVT_KEY_DOWN, self._on_key_down)

    def _on_key_down(self, event):
        key = event.GetKeyCode()
        idx = self.GetFocusedItem()
        if key == wx.WXK_SPACE:
            if idx >= 0:
                self.CheckItem(idx, not self.IsItemChecked(idx))
            event.Skip(False); return
        if key in (wx.WXK_RETURN, wx.WXK_NUMPAD_ENTER):
            self._open_in_browser(idx); event.Skip(False); return
        event.Skip()

    def _open_in_browser(self, idx):
        if 0 <= idx < len(self._entries):
            url = self._entries[idx].get("website") or self._entries[idx].get("play_url", "")
            if url:
                webbrowser.open(url)
                self.SetItem(idx, 7, "Wird gespielt")

    def populate(self, entries):
        self.DeleteAllItems()
        self._entries = entries
        for i, e in enumerate(entries):
            self.InsertItem(i, str(i + 1))
            self.SetItem(i, 1, e.get("station", ""))
            self.SetItem(i, 2, e.get("show", ""))
            self.SetItem(i, 3, e.get("title", ""))
            self.SetItem(i, 4, e.get("publish_date", ""))
            self.SetItem(i, 5, fmt_duration(e.get("duration", 0)))
            self.SetItem(i, 6, "✓" if e.get("url") else "")
            self.SetItem(i, 7, "")
        if self.GetItemCount() > 0:
            self.Focus(0); self.Select(0)

    def get_checked_indices(self):
        return [i for i in range(self.GetItemCount()) if self.IsItemChecked(i)]

    def set_all_checked(self, state: bool):
        for i in range(self.GetItemCount()):
            self.CheckItem(i, state)

    def get_entry(self, idx):
        return self._entries[idx] if 0 <= idx < len(self._entries) else {}

    def mark_status(self, idx, text):
        wx.CallAfter(self.SetItem, idx, 7, text)


# ------------------------------------------------------------------ #
#  Hauptfenster                                                       #
# ------------------------------------------------------------------ #
class AudiothekDownloader(wx.Frame):
    def __init__(self):
        super().__init__(None, title="ARD Audiothek Downloader", size=(1060, 790))
        self.is_searching = self.is_downloading = False
        self._search_timer = None
        self.SetBackgroundColour(wx.Colour(BG))
        self._build_ui()
        self.Centre()
        self.Show()

    def _build_ui(self):
        root = wx.Panel(self)
        root.SetBackgroundColour(wx.Colour(BG))
        vbox = wx.BoxSizer(wx.VERTICAL)

        # Header
        hdr = wx.Panel(root); hdr.SetBackgroundColour(wx.Colour(PANEL))
        hs = wx.BoxSizer(wx.HORIZONTAL)
        logo = wx.StaticText(hdr, label="ARD AUDIOTHEK DOWNLOADER")
        logo.SetForegroundColour(wx.Colour(ACCENT))
        f = logo.GetFont(); f.SetPointSize(16); f.SetWeight(wx.FONTWEIGHT_BOLD); logo.SetFont(f)
        self.sel_count = wx.StaticText(hdr, label="0 ausgewählt")
        self.sel_count.SetForegroundColour(wx.Colour(SUBTEXT))
        hs.Add(logo, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=12)
        hs.AddStretchSpacer()
        hs.Add(self.sel_count, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=12)
        hdr.SetSizer(hs); vbox.Add(hdr, flag=wx.EXPAND)

        # Suchleiste
        sp = wx.Panel(root); sp.SetBackgroundColour(wx.Colour(PANEL))
        ss = wx.BoxSizer(wx.HORIZONTAL)
        lbl_s = wx.StaticText(sp, label="Suche:")
        lbl_s.SetForegroundColour(wx.Colour(SUBTEXT))
        ss.Add(lbl_s, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)

        self.search_text = wx.TextCtrl(sp, style=wx.TE_PROCESS_ENTER, size=(-1, 38))
        self.search_text.SetHint("Suchbegriff eingeben und Enter drücken…")
        self._sc(self.search_text)
        self.search_text.Bind(wx.EVT_TEXT_ENTER, self.on_search)

        self.result_count_choice = wx.Choice(
            sp, choices=["25 Ergebnisse", "50 Ergebnisse", "100 Ergebnisse", "200 Ergebnisse"])
        self.result_count_choice.SetSelection(1)
        self.result_count_choice.SetMinSize((155, 38))
        self._sc(self.result_count_choice)

        self.search_btn = self._btn(sp, "Suchen", self.on_search, accent=True)
        ss.Add(self.search_text, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=8)
        ss.Add(self.result_count_choice, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=8)
        ss.Add(self.search_btn, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=8)
        sp.SetSizer(ss); vbox.Add(sp, flag=wx.EXPAND)

        # Statuszeile
        sb = wx.Panel(root); sb.SetBackgroundColour(wx.Colour("#12121a"))
        sbs = wx.BoxSizer(wx.HORIZONTAL)
        self.status_lbl = wx.StaticText(sb, label="Bereit.")
        self.status_lbl.SetForegroundColour(wx.Colour(ACCENT2))
        f2 = self.status_lbl.GetFont(); f2.SetPointSize(9); self.status_lbl.SetFont(f2)
        self.gauge = wx.Gauge(sb, range=100, size=(-1, 8))
        sbs.Add(self.status_lbl, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=6)
        sbs.Add(self.gauge, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=6)
        sb.SetSizer(sbs); vbox.Add(sb, flag=wx.EXPAND)

        hint = wx.StaticText(root,
            label="Pfeiltasten: navigieren  |  Leertaste: auswählen/abwählen  |  Enter: im Browser öffnen")
        hint.SetForegroundColour(wx.Colour(SUBTEXT))
        vbox.Add(hint, flag=wx.LEFT | wx.TOP | wx.BOTTOM, border=6)

        # Ergebnisliste
        self.result_list = AudiothekResultList(root)
        self.result_list.Bind(wx.EVT_LIST_ITEM_CHECKED,   self._on_check)
        self.result_list.Bind(wx.EVT_LIST_ITEM_UNCHECKED, self._on_check)
        self.result_list.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._on_activate)
        vbox.Add(self.result_list, proportion=1,
                 flag=wx.EXPAND | wx.LEFT | wx.RIGHT | wx.BOTTOM, border=6)

        vbox.Add(wx.StaticLine(root), flag=wx.EXPAND | wx.LEFT | wx.RIGHT, border=10)

        # Im Browser öffnen
        brp = wx.Panel(root); brp.SetBackgroundColour(wx.Colour(PANEL))
        brps = wx.BoxSizer(wx.HORIZONTAL)
        brps.AddStretchSpacer()
        brps.Add(self._btn(brp, "Im Browser öffnen  (Enter)", self.on_open_browser), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4)
        brp.SetSizer(brps); vbox.Add(brp, flag=wx.EXPAND)

        # Format-Optionen
        op = wx.Panel(root); op.SetBackgroundColour(wx.Colour(PANEL))
        ops = wx.BoxSizer(wx.HORIZONTAL)
        ops.Add(self._lbl(op, "Format:"), flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=10)
        self.format_choice = wx.Choice(op, choices=["mp3", "m4a", "ogg"])
        self.format_choice.SetSelection(0); self._sc(self.format_choice)
        ops.Add(self.format_choice, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=6)
        ops.Add(self._lbl(op, "Audio kbps:"), flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)
        self.audio_choice = wx.Choice(op, choices=["96", "128", "160", "192", "256", "320"])
        self.audio_choice.SetSelection(3); self._sc(self.audio_choice)
        ops.Add(self.audio_choice, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=6)
        info = wx.StaticText(op, label="ℹ  ARD Audiothek – Zwei-Stufen-Suche (Items + Sendungs-Episoden)")
        info.SetForegroundColour(wx.Colour(ACCENT2))
        ops.AddStretchSpacer(); ops.Add(info, flag=wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border=12)
        op.SetSizer(ops); vbox.Add(op, flag=wx.EXPAND)

        # Speicherordner
        bp = wx.Panel(root); bp.SetBackgroundColour(wx.Colour(PANEL))
        bps = wx.BoxSizer(wx.HORIZONTAL)
        lbl_f = wx.StaticText(bp, label="Speicherordner:")
        lbl_f.SetForegroundColour(wx.Colour(SUBTEXT))
        bps.Add(lbl_f, flag=wx.ALIGN_CENTER_VERTICAL | wx.LEFT, border=8)
        self.folder_text = wx.TextCtrl(bp, value=load_download_dir()); self._sc(self.folder_text)
        self.date_cb = wx.CheckBox(bp, label="Datum als Unterordner", style=wx.CHK_2STATE)
        bps.Add(self.folder_text, proportion=1, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=8)
        bps.Add(self._btn(bp, "Ordner wählen", self.on_browse), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4)
        bps.Add(self.date_cb, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=6)
        bp.SetSizer(bps); vbox.Add(bp, flag=wx.EXPAND)

        # Aktions-Buttons (eigene Zeile)
        ap = wx.Panel(root); ap.SetBackgroundColour(wx.Colour(PANEL))
        aps = wx.BoxSizer(wx.HORIZONTAL)
        aps.AddStretchSpacer()
        aps.Add(self._btn(ap, "Alle auswählen",  self.on_sel_all),  flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4)
        aps.Add(self._btn(ap, "Keine auswählen", self.on_sel_none), flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=4)
        self.dl_btn = self._btn(ap, "Download starten  (Strg+D)", self.on_download, accent=True)
        aps.Add(self.dl_btn, flag=wx.ALL | wx.ALIGN_CENTER_VERTICAL, border=8)
        ap.SetSizer(aps); vbox.Add(ap, flag=wx.EXPAND)

        root.SetSizer(vbox)

        # Strg+D → Download starten
        accel = wx.AcceleratorTable([
            (wx.ACCEL_CTRL, ord("D"), self.dl_btn.GetId())
        ])
        self.SetAcceleratorTable(accel)

    # ── Hilfsmethoden ────────────────────────────────────────────────
    def _sc(self, c):
        c.SetBackgroundColour(wx.Colour(ITEM_BG))
        c.SetForegroundColour(wx.Colour(TEXT))

    def _lbl(self, p, t):
        l = wx.StaticText(p, label=t); l.SetForegroundColour(wx.Colour(SUBTEXT)); return l

    def _btn(self, p, label, handler, accent=False):
        b = wx.Button(p, label=label)
        b.SetBackgroundColour(wx.Colour(ACCENT if accent else ITEM_BG))
        b.SetForegroundColour(wx.Colour("#fff" if accent else TEXT))
        b.SetMinSize((-1, 36)); b.Bind(wx.EVT_BUTTON, handler); return b

    def _status(self, msg, v=None):
        wx.CallAfter(self.status_lbl.SetLabel, msg)
        if v is not None: wx.CallAfter(self.gauge.SetValue, int(v))

    def _upd_sel(self):
        n = len(self.result_list.get_checked_indices())
        wx.CallAfter(self.sel_count.SetLabel, f"{n} ausgewählt")

    def _on_check(self, e):   self._upd_sel(); e.Skip()
    def _on_activate(self, e):
        entry = self.result_list.get_entry(e.GetIndex())
        url = entry.get("website") or entry.get("play_url", "")
        if url:
            webbrowser.open(url)
            self.result_list.mark_status(e.GetIndex(), "Wird gespielt")

    # ── Suchfortschritt-Timer ────────────────────────────────────────
    def _start_timer(self):
        self._tick = 0
        dots = [".", "..", "..."]
        def tick():
            if not self.is_searching: return
            self._tick += 1
            wx.CallAfter(self._status, f"Suche läuft{dots[self._tick % 3]}  (bitte warten)")
            wx.CallAfter(self.gauge.SetValue, (self._tick * 7) % 100)
            self._search_timer = threading.Timer(2.0, tick)
            self._search_timer.daemon = True
            self._search_timer.start()
        self._search_timer = threading.Timer(2.0, tick)
        self._search_timer.daemon = True
        self._search_timer.start()

    def _stop_timer(self):
        if self._search_timer:
            self._search_timer.cancel(); self._search_timer = None

    # ── Suche ────────────────────────────────────────────────────────
    def on_search(self, event):
        if self.is_searching or self.is_downloading: return
        q = self.search_text.GetValue().strip()
        if not q:
            wx.MessageBox("Bitte einen Suchbegriff eingeben.", "Hinweis", wx.ICON_INFORMATION)
            self.search_text.SetFocus(); return
        n = [25, 50, 100, 200][self.result_count_choice.GetSelection()]
        self.is_searching = True
        self.search_btn.Disable()
        self._status("Suche wird gestartet…", 0)
        self._start_timer()
        threading.Thread(target=self._search_t, args=(q, n), daemon=True).start()

    def _search_t(self, q, n):
        try:
            entries = search_audiothek(q, n)
            wx.CallAfter(self._populate, entries)
        except Exception as e:
            wx.CallAfter(self._search_err, str(e))

    def _populate(self, entries):
        self._stop_timer()
        self.result_list.populate(entries)
        c = len(entries)
        self._status(f"Suche abgeschlossen  –  {c} Ergebnisse gefunden.", 100)
        self.is_searching = False
        self.search_btn.Enable()
        self._upd_sel()
        if c == 0:
            wx.MessageBox("Keine Ergebnisse gefunden.", "Suche", wx.ICON_INFORMATION)
            self.search_text.SetFocus()
        else:
            wx.Bell(); self.result_list.SetFocus()

    def _search_err(self, msg):
        self._stop_timer()
        self.is_searching = False
        self.search_btn.Enable()
        self._status("Suche fehlgeschlagen.", 0)
        wx.MessageBox(f"Suche fehlgeschlagen:\n{msg}", "Fehler", wx.ICON_ERROR)

    # ── Auswahl ──────────────────────────────────────────────────────
    def on_open_browser(self, e):
        idx = self.result_list.GetFocusedItem()
        if idx >= 0:
            self.result_list._open_in_browser(idx)

    def on_sel_all(self, e):  self.result_list.set_all_checked(True);  self._upd_sel()
    def on_sel_none(self, e): self.result_list.set_all_checked(False); self._upd_sel()

    def on_browse(self, e):
        with wx.DirDialog(self, "Ordner wählen") as dlg:
            if dlg.ShowModal() == wx.ID_OK:
                f = dlg.GetPath()
                self.folder_text.SetValue(f)
                save_download_dir(f)

    # ── Download ─────────────────────────────────────────────────────
    def on_download(self, event):
        checked = self.result_list.get_checked_indices()
        if not checked:
            wx.MessageBox("Bitte mindestens eine Episode mit der Leertaste auswählen.",
                          "Hinweis", wx.ICON_WARNING)
            self.result_list.SetFocus(); return
        if self.is_downloading or self.is_searching: return
        self.is_downloading = True
        self.dl_btn.Disable()
        folder = self.folder_text.GetValue()
        if self.date_cb.GetValue():
            from datetime import datetime
            folder = os.path.join(folder, datetime.now().strftime("%Y-%m-%d"))
        os.makedirs(folder, exist_ok=True)
        save_download_dir(folder)
        params = {"folder": folder,
                  "fmt":    self.format_choice.GetStringSelection(),
                  "abr":    self.audio_choice.GetStringSelection()}
        threading.Thread(target=self._dl_t, args=(checked, params), daemon=True).start()

    def _dl_t(self, selections, params):
        folder = params["folder"]
        fmt    = params["fmt"]
        abr    = params["abr"]
        total  = len(selections)

        for i, sel in enumerate(selections):
            entry = self.result_list.get_entry(sel)
            title = sanitize_filename(entry.get("title", f"Episode_{sel+1}"))
            show  = sanitize_filename(entry.get("show", ""))
            date  = entry.get("publish_date", "")
            base  = f"{show} - {title}" if show else title
            if date: base += f" ({date})"
            url   = entry.get("url", "")
            if not url:
                wx.CallAfter(wx.MessageBox, f"Keine URL für:\n{title}", "Fehler", wx.ICON_WARNING)
                continue

            wx.CallAfter(self._status, f"Starte Download {i+1}/{total}: {base[:55]}", 0)
            wx.CallAfter(self.result_list.mark_status, sel, "Lädt…")

            def hook(d, _i=i, _t=total, _s=sel):
                if d["status"] == "downloading":
                    pct = 0
                    try: pct = float(d.get("_percent_str","0").strip().replace("%",""))
                    except: pass
                    wx.CallAfter(self._status,
                        f"Download {_i+1}/{_t}:  {d.get('_percent_str','').strip()}  "
                        f"{d.get('_speed_str','')}  ETA {d.get('_eta_str','')}", pct)
                elif d["status"] == "finished":
                    wx.CallAfter(self._status, f"Download {_i+1}/{_t} fertig, konvertiere…", 100)
                    wx.CallAfter(self.result_list.mark_status, _s, "Konvertiert…")

            opts = {
                "outtmpl":        os.path.join(folder, f"{base}.%(ext)s"),
                "progress_hooks": [hook],
                "quiet":          True,
                "postprocessors": [{"key": "FFmpegExtractAudio",
                                    "preferredcodec":   fmt,
                                    "preferredquality": abr}],
            }
            try:
                with ytdl.YoutubeDL(opts) as ydl:
                    ydl.download([url])
                wx.CallAfter(self.result_list.mark_status, sel, "✓ Fertig")
            except Exception as e:
                wx.CallAfter(self.result_list.mark_status, sel, "✗ Fehler")
                wx.CallAfter(wx.MessageBox, f"Fehler bei:\n{base}\n\n{e}",
                             "Download-Fehler", wx.ICON_ERROR)

        wx.CallAfter(self._done, total, folder)

    def _done(self, total, folder):
        self.is_downloading = False
        self.dl_btn.Enable()
        self._status(f"{total} Downloads abgeschlossen.", 100)
        wx.MessageBox(f"{total} Downloads abgeschlossen!\n\nDateien gespeichert in:\n{folder}",
                      "Downloads fertig", wx.ICON_INFORMATION)


# ------------------------------------------------------------------ #
#  Start                                                              #
# ------------------------------------------------------------------ #
if __name__ == "__main__":
    app = wx.App(False)
    ensure_ffmpeg()
    frame = AudiothekDownloader()
    app.MainLoop()
