#!/usr/bin/env python3
"""
YouTube downloader — single-file local app (Windows, no console window).

Requirements (install once, from a Command Prompt):
    pip install -U "yt-dlp[default]"
    (ffmpeg must also be installed and on PATH)

Double-click this file (or the desktop shortcut created by
"Create Desktop Shortcut.vbs") to launch it. Your browser opens
automatically with a URL box and a Download button.

Only download videos you have the rights to (your own uploads, public
domain, Creative Commons, or otherwise permitted content).
"""

import json
import os
import shutil
import socket
import subprocess
import threading
import webbrowser
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from urllib.parse import urlparse, parse_qs, quote

PORT = 5455
ALLOWED_BROWSERS = {"chrome", "edge", "firefox", "brave", "opera", "vivaldi", "safari", "chromium", "whale"}
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
DOWNLOAD_DIR = os.path.join(SCRIPT_DIR, "downloads")
os.makedirs(DOWNLOAD_DIR, exist_ok=True)

PAGE = """<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>YouTube Downloader</title>
<style>
  :root {
    --bg: #14161a;
    --panel: #1d2027;
    --panel-2: #262a33;
    --border: #333844;
    --text: #eef0f3;
    --text-dim: #9aa1ad;
    --accent: #e0a72e;
    --accent-hover: #f0b93f;
    --radius: 10px;
  }
  * { box-sizing: border-box; }
  body {
    margin: 0;
    font-family: "Segoe UI", system-ui, -apple-system, sans-serif;
    background: var(--bg);
    color: var(--text);
  }
  .wrap { max-width: 640px; margin: 0 auto; padding: 32px 20px; }
  h1 { font-size: 1.4rem; margin: 0 0 20px; }
  h2 { font-size: 1.05rem; margin: 28px 0 12px; color: var(--text-dim); }
  form { display: flex; gap: 10px; flex-wrap: wrap; }
  input[type="url"], select {
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 12px;
    font-size: 0.95rem;
  }
  input[type="url"] { flex: 1; min-width: 220px; }
  .btn {
    background: var(--panel-2);
    color: var(--text);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    padding: 10px 18px;
    font-size: 0.95rem;
    cursor: pointer;
    transition: background 0.15s, border-color 0.15s;
  }
  .btn:hover:not(:disabled) { background: #30353f; border-color: #454b58; }
  .btn:disabled { opacity: 0.45; cursor: not-allowed; }
  .btn-primary {
    background: var(--accent);
    color: #1a1a1a;
    border-color: var(--accent);
    font-weight: 600;
  }
  .btn-primary:hover:not(:disabled) { background: var(--accent-hover); }
  #log {
    background: var(--panel);
    border: 1px solid var(--border);
    color: var(--text-dim);
    font-family: ui-monospace, monospace;
    font-size: 0.85rem;
    padding: 12px;
    height: 220px;
    overflow-y: auto;
    white-space: pre-wrap;
    border-radius: var(--radius);
    margin-top: 16px;
    display: none;
  }
  #files { list-style: none; padding: 0; margin: 0; }
  #files li {
    display: flex;
    justify-content: space-between;
    align-items: center;
    padding: 10px 12px;
    background: var(--panel);
    border: 1px solid var(--border);
    border-radius: var(--radius);
    margin-bottom: 6px;
  }
  #files a { color: var(--text); text-decoration: none; }
  #files a:hover { color: var(--accent); }
  .size { color: var(--text-dim); font-size: 0.8rem; margin-left: 10px; flex-shrink: 0; }
  .muted { color: var(--text-dim); font-size: 0.85rem; }
</style>
</head>
<body>
<div class="wrap">
  <h1>🎬 YouTube Downloader</h1>
  <form id="form">
    <input id="url" type="url" placeholder="https://youtube.com/watch?v=..." required />
    <select id="browser" title="Use this browser's login cookies (needed for age-restricted videos)">
      <option value="">No cookies</option>
      <option value="chrome">Cookies: Chrome</option>
      <option value="edge">Cookies: Edge</option>
      <option value="firefox">Cookies: Firefox</option>
      <option value="brave">Cookies: Brave</option>
      <option value="opera">Cookies: Opera</option>
      <option value="__file__">Cookies: cookies.txt file</option>
    </select>
    <button id="submitBtn" class="btn btn-primary" type="submit">Download</button>
  </form>
  <div id="log"></div>

  <h2>Downloaded files</h2>
  <ul id="files"></ul>
</div>
<script>
  const form = document.getElementById("form");
  const urlInput = document.getElementById("url");
  const browserSelect = document.getElementById("browser");
  const submitBtn = document.getElementById("submitBtn");
  const log = document.getElementById("log");
  const filesEl = document.getElementById("files");

  function appendLog(text) {
    log.style.display = "block";
    log.textContent += text + "\\n";
    log.scrollTop = log.scrollHeight;
  }

  function formatSize(bytes) {
    if (bytes < 1024) return bytes + " B";
    const units = ["KB", "MB", "GB"];
    let val = bytes, i = -1;
    do { val /= 1024; i++; } while (val >= 1024 && i < units.length - 1);
    return val.toFixed(1) + " " + units[i];
  }

  async function loadFiles() {
    const res = await fetch("/files");
    const files = await res.json();
    filesEl.innerHTML = files.map(f =>
      `<li><a href="/file?name=${encodeURIComponent(f.name)}">${f.name}</a>
       <span class="size">${formatSize(f.size)}</span></li>`
    ).join("") || `<li class="muted">No downloads yet.</li>`;
  }

  form.addEventListener("submit", (evt) => {
    evt.preventDefault();
    const url = urlInput.value.trim();
    if (!url) return;
    submitBtn.disabled = true;
    log.textContent = "";
    appendLog("Starting download...");

    const browser = browserSelect.value;
    const qs = "url=" + encodeURIComponent(url) + (browser ? "&browser=" + encodeURIComponent(browser) : "");
    const source = new EventSource("/download?" + qs);
    source.addEventListener("log", (e) => appendLog(e.data));
    source.addEventListener("done", (e) => {
      appendLog(e.data === "0" ? "\\nDone." : `\\nExited with code ${e.data}`);
      submitBtn.disabled = false;
      source.close();
      loadFiles();
    });
    source.onerror = () => {
      appendLog("\\nConnection lost.");
      submitBtn.disabled = false;
      source.close();
    };
  });

  loadFiles();
</script>
</body>
</html>
"""


class Handler(BaseHTTPRequestHandler):
    def log_message(self, *args):
        pass

    def do_GET(self):
        parsed = urlparse(self.path)
        if parsed.path == "/":
            self._serve_page()
        elif parsed.path == "/download":
            query = parse_qs(parsed.query)
            self._serve_download(query.get("url", [None])[0], query.get("browser", [None])[0])
        elif parsed.path == "/files":
            self._serve_files()
        elif parsed.path == "/file":
            self._serve_file(parse_qs(parsed.query).get("name", [None])[0])
        else:
            self.send_error(404)

    def _serve_page(self):
        body = PAGE.encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "text/html; charset=utf-8")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _serve_download(self, url, browser):
        if not url:
            self.send_error(400, "Missing url")
            return

        self.send_response(200)
        self.send_header("Content-Type", "text/event-stream")
        self.send_header("Cache-Control", "no-cache")
        self.send_header("Connection", "keep-alive")
        self.end_headers()

        def sse(event, data):
            for line in str(data).split("\n"):
                self.wfile.write(f"data: {line}\n".encode("utf-8"))
            self.wfile.write(f"event: {event}\n\n".encode("utf-8"))
            self.wfile.flush()

        if not shutil.which("yt-dlp"):
            sse("log", "yt-dlp is not installed. Open a Command Prompt and run: pip install -U \"yt-dlp[default]\"")
            sse("done", "1")
            return

        args = [
            "yt-dlp",
            "--format", "bv*+ba/b",
            "--merge-output-format", "mp4",
            "--newline",
            "--no-playlist",
            "--js-runtimes", "node",
        ]

        if browser == "__file__":
            cookies_path = os.path.join(SCRIPT_DIR, "cookies.txt")
            if not os.path.isfile(cookies_path):
                sse("log", f"No cookies.txt found at {cookies_path}")
                sse("log", "Export one with a browser extension (e.g. \"Get cookies.txt LOCALLY\") "
                            "and save it next to this script as cookies.txt")
                sse("done", "1")
                return
            args += ["--cookies", cookies_path]
            sse("log", "Using cookies.txt")
        elif browser in ALLOWED_BROWSERS:
            args += ["--cookies-from-browser", browser]
            sse("log", f"Using cookies from {browser} (close it first if this fails to read them)")

        args += [
            "--output", os.path.join(DOWNLOAD_DIR, "%(title)s [%(id)s].%(ext)s"),
            url,
        ]

        try:
            proc = subprocess.Popen(
                args, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                text=True, bufsize=1, creationflags=subprocess.CREATE_NO_WINDOW,
            )
        except OSError as err:
            sse("log", f"Failed to start yt-dlp: {err}")
            sse("done", "1")
            return

        try:
            for line in proc.stdout:
                sse("log", line.rstrip("\n"))
        except (BrokenPipeError, ConnectionResetError):
            proc.kill()
            return

        proc.wait()
        sse("done", str(proc.returncode))

    def _serve_files(self):
        files = []
        for name in os.listdir(DOWNLOAD_DIR):
            full = os.path.join(DOWNLOAD_DIR, name)
            if os.path.isfile(full):
                files.append({"name": name, "size": os.path.getsize(full),
                              "mtime": os.path.getmtime(full)})
        files.sort(key=lambda f: f["mtime"], reverse=True)
        body = json.dumps(files).encode("utf-8")
        self.send_response(200)
        self.send_header("Content-Type", "application/json")
        self.send_header("Content-Length", str(len(body)))
        self.end_headers()
        self.wfile.write(body)

    def _serve_file(self, name):
        if not name:
            self.send_error(400)
            return
        full = os.path.join(DOWNLOAD_DIR, name)
        if os.path.dirname(full) != DOWNLOAD_DIR or not os.path.isfile(full):
            self.send_error(404)
            return
        self.send_response(200)
        self.send_header("Content-Type", "application/octet-stream")
        self.send_header("Content-Disposition", f"attachment; filename=\"{quote(name)}\"")
        self.send_header("Content-Length", str(os.path.getsize(full)))
        self.end_headers()
        with open(full, "rb") as f:
            shutil.copyfileobj(f, self.wfile)


def port_in_use(port):
    with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
        return sock.connect_ex(("127.0.0.1", port)) == 0


def main():
    url = f"http://127.0.0.1:{PORT}/"

    if port_in_use(PORT):
        # Already running (e.g. the icon was double-clicked twice) —
        # just open another browser tab to it.
        webbrowser.open(url)
        return

    server = ThreadingHTTPServer(("127.0.0.1", PORT), Handler)
    threading.Timer(0.5, lambda: webbrowser.open(url)).start()
    server.serve_forever()


if __name__ == "__main__":
    main()
