#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import os
import sys
import subprocess
import threading
import shutil
import re
import queue
import tempfile
import time
import json
import hashlib
import base64
import random
import string
import locale
import urllib.request
from datetime import datetime
from urllib.parse import urlencode, urlparse
from http.cookiejar import CookieJar

import ipaddress
from flask import Flask, request, jsonify, Response, render_template_string, send_from_directory, session

app = Flask(__name__)
app.secret_key = 'Ga2011422%_secret_key'

GUEST_DIR = "/mnt/sda1/guest_downloads"

failed_attempts = {}
banned_ips = set()

def get_real_ip():
    xf = request.headers.get('X-Forwarded-For')
    if xf: return xf.split(',')[0].strip()
    xr = request.headers.get('X-Real-IP')
    if xr: return xr.strip()
    return request.remote_addr or ""

def is_local_ip(ip_str):
    try:
        ip = ipaddress.ip_address(ip_str)
        return ip.is_private or ip.is_loopback or ip.is_unspecified
    except ValueError:
        return False


# --- Guest Cleanup Thread ---
def guest_cleanup_loop():
    while True:
        time.sleep(300)
        if os.path.exists(GUEST_DIR):
            now = time.time()
            for f in os.listdir(GUEST_DIR):
                p = os.path.join(GUEST_DIR, f)
                if os.path.isfile(p) and now - os.path.getmtime(p) > 600: # 10 minutes
                    try: os.remove(p)
                    except: pass
threading.Thread(target=guest_cleanup_loop, daemon=True).start()

@app.before_request
def block_banned_ips():
    ip = get_real_ip()
    if ip in banned_ips:
        return jsonify({"status": "error", "message": "Access Denied: Your IP is banned."}), 403

# --- Core Logic from main.py ---

class XBogus:
    def __init__(self, user_agent=None):
        self._array = [
            None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,
            None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,
            None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,
            0, 1, 2, 3, 4, 5, 6, 7, 8, 9, None, None, None, None, None, None, None, None, None, None, None,
            None, None, None, None, None, None, None, None, None, None, None, None, None, None, None, None,
            None, None, None, None, None, None, None, None, None, None, None, None, 10, 11, 12, 13, 14, 15
        ]
        self._character = "Dkdpgh4ZKsQB80/Mfvw36XI1R25-WUAlEi7NLboqYTOPuzmFjJnryx9HVGcaStCe="
        self._ua_key = b"\x00\x01\x0c"
        self._user_agent = (
            user_agent if user_agent else
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
        )

    def _md5_str_to_array(self, md5_str):
        if isinstance(md5_str, str) and len(md5_str) > 32:
            return [ord(char) for char in md5_str]
        array = []
        idx = 0
        while idx < len(md5_str):
            array.append((self._array[ord(md5_str[idx])] << 4) | self._array[ord(md5_str[idx + 1])])
            idx += 2
        return array

    def _md5(self, input_data):
        if isinstance(input_data, str):
            data = self._md5_str_to_array(input_data)
        else:
            data = input_data
        md5_hash = hashlib.md5()
        md5_hash.update(bytes(data))
        return md5_hash.hexdigest()

    def _md5_encrypt(self, url_path):
        hashed = self._md5(self._md5_str_to_array(self._md5(url_path)))
        return self._md5_str_to_array(hashed)

    def _encoding_conversion(self, a, b, c, e, d, t, f, r, n, o, i, _, x, u, s, l, v, h, p):
        payload = [a]
        payload.append(int(i))
        payload.extend([b, _, c, x, e, u, d, s, t, l, f, v, r, h, n, p, o])
        return bytes(payload).decode("ISO-8859-1")

    def _encoding_conversion2(self, a, b, c):
        return chr(a) + chr(b) + c

    @staticmethod
    def _rc4_encrypt(key, data):
        s = list(range(256))
        j = 0
        encrypted = bytearray()
        for i in range(256):
            j = (j + s[i] + key[i % len(key)]) % 256
            s[i], s[j] = s[j], s[i]
        i = j = 0
        for byte in data:
            i = (i + 1) % 256
            j = (j + s[i]) % 256
            s[i], s[j] = s[j], s[i]
            encrypted.append(byte ^ s[(s[i] + s[j]) % 256])
        return encrypted

    def _calculation(self, a1, a2, a3):
        x3 = ((a1 & 255) << 16) | ((a2 & 255) << 8) | (a3 & 255)
        return (
            self._character[(x3 & 16515072) >> 18]
            + self._character[(x3 & 258048) >> 12]
            + self._character[(x3 & 4032) >> 6]
            + self._character[x3 & 63]
        )

    def build(self, url):
        ua_md5_array = self._md5_str_to_array(
            self._md5(
                base64.b64encode(
                    self._rc4_encrypt(self._ua_key, self._user_agent.encode("ISO-8859-1"))
                ).decode("ISO-8859-1")
            )
        )
        empty_md5_array = self._md5_str_to_array(
            self._md5(self._md5_str_to_array("d41d8cd98f00b204e9800998ecf8427e"))
        )
        url_md5_array = self._md5_encrypt(url)
        timer = int(time.time())
        ct = 536919696
        new_array = [
            64, 0.00390625, 1, 12,
            url_md5_array[14], url_md5_array[15],
            empty_md5_array[14], empty_md5_array[15],
            ua_md5_array[14], ua_md5_array[15],
            timer >> 24 & 255, timer >> 16 & 255, timer >> 8 & 255, timer & 255,
            ct >> 24 & 255, ct >> 16 & 255, ct >> 8 & 255, ct & 255,
        ]
        xor_result = new_array[0]
        for value in new_array[1:]:
            if isinstance(value, float):
                value = int(value)
            xor_result ^= value
        new_array.append(xor_result)
        array3 = []
        array4 = []
        idx = 0
        while idx < len(new_array):
            value = new_array[idx]
            array3.append(value)
            if idx + 1 < len(new_array):
                array4.append(new_array[idx + 1])
            idx += 2
        merged = array3 + array4
        garbled = self._encoding_conversion2(
            2, 255,
            self._rc4_encrypt(
                "ÿ".encode("ISO-8859-1"),
                self._encoding_conversion(*merged).encode("ISO-8859-1"),
            ).decode("ISO-8859-1"),
        )
        xb = ""
        idx = 0
        while idx < len(garbled):
            xb += self._calculation(ord(garbled[idx]), ord(garbled[idx + 1]), ord(garbled[idx + 2]))
            idx += 3
        signed_url = f"{url}&X-Bogus={xb}"
        return signed_url, xb, self._user_agent


class HeadlessDownloader:
    def __init__(self):
        self.is_debug = True
        self.resolved_url = ""
        self.current_process = None
        self.transcode_process = None
        self.stop_event = threading.Event()
        self.last_downloaded_file = None
        
        self.log_queue = queue.Queue()
        self.yt_dlp_path = shutil.which("yt-dlp") or "yt-dlp"
        self.ffmpeg_path = shutil.which("ffmpeg") or "ffmpeg"
        self.bbdown_path = shutil.which("BBDown") or "BBDown"
        
        threading.Thread(target=self._init_tools_background, daemon=True).start()

    _DOUYIN_HOSTS = (
        "douyin.com", "www.douyin.com", "v.douyin.com",
        "v.iesdouyin.com", "iesdouyin.com", "live.douyin.com",
    )
    _DOUYIN_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36"
    _douyin_cookies = None

    def log(self, message):
        timestamp = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
        full_msg = f"{timestamp} - {message}"
        print(full_msg)
        self.log_queue.put(full_msg)

    def _extract_url_from_text(self, text):
        urls = re.findall(r'https?://[^\s<>"\'`]+', text)
        if not urls:
            return text.strip()
        for u in urls:
            u = u.rstrip('.,;:!?)')
            try:
                parsed = urlparse(u)
                host = (parsed.netloc or "").lower()
                for dh in self._DOUYIN_HOSTS:
                    if host == dh or host.endswith("." + dh):
                        return u
            except Exception:
                continue
        return urls[0].rstrip('.,;:!?)')

    def is_douyin_url(self, url):
        try:
            parsed = urlparse(url)
            host = (parsed.netloc or "").lower()
            for dh in self._DOUYIN_HOSTS:
                if host == dh or host.endswith("." + dh):
                    return True
        except Exception:
            pass
        if re.search(r'v\.douyin\.com/\w+', url):
            return True
        if re.search(r'https?://[^\s]*douyin\.com[^\s]*', url):
            return True
        return False
        
    def is_bilibili_url(self, url):
        try:
            parsed = urlparse(url)
            host = (parsed.netloc or "").lower()
            return host.endswith("bilibili.com") or host.endswith("b23.tv")
        except Exception:
            return False

    def _douyin_resolve_short_url(self, short_url):
        if not short_url.lower().startswith(("http://", "https://")):
            short_url = "https://" + short_url
        try:
            req = urllib.request.Request(short_url, method="GET")
            req.add_header("User-Agent", self._DOUYIN_UA)
            opener = urllib.request.build_opener(urllib.request.HTTPRedirectHandler)
            resp = opener.open(req, timeout=10)
            return resp.url
        except Exception as e:
            return short_url

    def _douyin_fetch_cookies(self):
        if self._douyin_cookies:
            return self._douyin_cookies
        try:
            cookie_jar = CookieJar()
            opener = urllib.request.build_opener(urllib.request.HTTPCookieProcessor(cookie_jar))
            ttwid_payload = json.dumps({
                "region": "cn", "aid": 1768, "needFid": False, "service": "www.douyin.com",
                "migrate_info": {"ticket": "", "source": "node"}, "cbUrlProtocol": "https", "union": True,
            }).encode("utf-8")
            req = urllib.request.Request("https://ttwid.bytedance.com/ttwid/union/register/", data=ttwid_payload, method="POST")
            req.add_header("User-Agent", self._DOUYIN_UA)
            req.add_header("Content-Type", "application/json")
            opener.open(req, timeout=10)
            cookies = {cookie.name: cookie.value for cookie in cookie_jar}
            self._douyin_cookies = cookies
            return cookies
        except Exception:
            return {}

    def _douyin_sanitize_filename(self, filename, max_length=80):
        filename = filename.replace("\n", " ").replace("\r", " ")
        filename = re.sub(r'[<>:"/\\|?*#\x00-\x1f]', "_", filename)
        filename = re.sub(r"_+", "_", filename)
        filename = re.sub(r" +", " ", filename)
        filename = filename.strip("._- ")
        if len(filename) > max_length:
            filename = filename[:max_length].rstrip("._- ")
        return filename or "untitled"

    def _douyin_download_file(self, url, save_path, ua=None):
        headers = {
            "User-Agent": ua or self._DOUYIN_UA,
            "Referer": "https://www.douyin.com/",
            "Accept": "*/*",
            "Accept-Encoding": "identity",
            "Origin": "https://www.douyin.com",
        }
        cookies = self._douyin_cookies or {}
        if cookies:
            headers["Cookie"] = "; ".join(f"{k}={v}" for k, v in cookies.items())
        req = urllib.request.Request(url, method="GET", headers=headers)
        tmp_path = None
        try:
            save_dir = os.path.dirname(save_path)
            os.makedirs(save_dir, exist_ok=True)
            fd, tmp_path = tempfile.mkstemp(suffix=".tmp", dir=save_dir)
            os.close(fd)
            with urllib.request.urlopen(req, timeout=120) as resp:
                total = int(resp.headers.get("Content-Length", 0))
                downloaded = 0
                last_pct = -1
                with open(tmp_path, "wb") as f:
                    while True:
                        if self.stop_event.is_set():
                            return False
                        chunk = resp.read(262144)
                        if not chunk: break
                        f.write(chunk)
                        downloaded += len(chunk)
                        if total > 0:
                            pct = downloaded * 100 // total
                            if pct >= last_pct + 5:
                                self.log(f"下载进度: {pct}% ({downloaded // 1024}KB / {total // 1024}KB)")
                                last_pct = pct
                        elif downloaded % (5 * 1024 * 1024) < 262144:
                            self.log(f"已下载: {downloaded // 1024}KB")
            os.replace(tmp_path, save_path)
            return True
        except Exception as e:
            self.log(f"抖音文件下载失败: {e}")
            return False
        finally:
            if tmp_path and os.path.exists(tmp_path):
                try: os.remove(tmp_path)
                except: pass

    def douyin_download(self, url, download_path):
        self.log("检测到抖音链接，使用无头浏览器抓取...")
        try:
            from playwright.sync_api import sync_playwright
        except ImportError:
            self.log("未安装 Playwright，无法使用抖音专用下载")
            return False

        parsed = urlparse(url)
        host = (parsed.netloc or "").lower()
        short_hosts = ("v.douyin.com", "v.iesdouyin.com", "iesdouyin.com")
        if any(host == h or host.endswith("." + h) for h in short_hosts) or re.search(r'v\.douyin\.com/\w+', url):
            self.log("正在解析抖音短链...")
            url = self._douyin_resolve_short_url(url)

        video_urls = []
        page_title = ""
        cdn_hosts = ("douyinvod.com", "bytevcloudtp.com", "bytecdn.cn", "byteimg.com", "bdurl.net")

        try:
            with sync_playwright() as p:
                browser = p.chromium.launch(headless=True)
                context = browser.new_context(user_agent=self._DOUYIN_UA)
                page = context.new_page()

                def on_response(response):
                    try:
                        resp_url = response.url
                        if not any(h in resp_url for h in cdn_hosts): return
                        ct = response.headers.get("content-type", "")
                        cl = int(response.headers.get("content-length", "0") or "0")
                        
                        is_video = "video" in ct or "mime_type=video" in resp_url
                        clean_url = re.sub(r'[&?]Range=[^&]*', '', resp_url)

                        if is_video: video_urls.append((clean_url, cl, response.status))
                    except: pass

                page.on("response", on_response)
                self.log("正在加载抖音页面...")
                page.goto(url, wait_until="domcontentloaded", timeout=30000)
                try: page_title = page.title() or ""
                except: pass

                self.log("等待视频加载...")
                for _ in range(10):
                    if self.stop_event.is_set(): return False
                    if video_urls: break
                    time.sleep(1)
                
                browser.close()
        except Exception as e:
            self.log(f"无头浏览器出错: {e}")
            return False

        if not video_urls:
            self.log("未抓取到视频地址")
            return False

        def _dedup_and_best(urls):
            seen = {}
            for u, cl, status in urls:
                base = u.split("?")[0]
                if base not in seen or cl > seen[base][1]:
                    seen[base] = (u, cl, status)
            items = list(seen.values())
            full = [x for x in items if x[2] == 200 and x[1] > 0]
            if full:
                full.sort(key=lambda t: t[1], reverse=True)
                return full[0][0]
            items.sort(key=lambda t: (-t[1], t[2]))
            return items[0][0]

        best_video = _dedup_and_best(video_urls)

        aweme_id = re.search(r"/video/(\d+)", url).group(1) if re.search(r"/video/(\d+)", url) else f"dy_{int(time.time())}"
        safe_title = self._douyin_sanitize_filename(page_title.replace("- 抖音", "").strip() or aweme_id)
        save_path = os.path.join(download_path, f"{safe_title}.mp4")

        self.log("开始下载视频（单流）...")
        if not self._douyin_download_file(best_video, save_path):
            return False

        if os.path.exists(save_path):
            self.log(f"下载完成: {save_path}")
            return True
        return False

    def bilibili_download(self, url, download_path):
        self.log(f"检测到Bilibili链接，调用 BBDown 下载: {url}")
        bbdown_exe = self.bbdown_path
        if not shutil.which(bbdown_exe):
            self.log("未找到 BBDown 命令，尝试 yt-dlp...")
            return False
            
        cmd = [bbdown_exe, "-tv", url, "--work-dir", download_path]
        ffmpeg_exe = shutil.which(self.ffmpeg_path)
        if ffmpeg_exe: cmd.extend(["--ffmpeg-path", ffmpeg_exe])
            
        try:
            self.current_process = subprocess.Popen(
                cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                text=True, encoding="utf-8", errors="replace", stdin=subprocess.DEVNULL, bufsize=1
            )
            proc = self.current_process
            while True:
                if self.stop_event.is_set(): break
                line = proc.stdout.readline()
                if not line: break
                if line.strip(): self.log(line.strip())
                time.sleep(0.001)
                
            proc.wait()
            if self.stop_event.is_set(): return True
            if proc.returncode == 0:
                self.log("Bilibili 下载完成！")
                return True
            return False
        except Exception as e:
            self.log(f"BBDown 执行出错: {str(e)}")
            return False

    def _init_tools_background(self):
        self._ytdlp_ready.set()

    def stop_download(self):
        self.stop_event.set()
        self.log("正在终止任务...")
        if self.current_process:
            try: self.current_process.kill()
            except: pass
        if self.transcode_process:
            try: self.transcode_process.kill()
            except: pass

    def resolve_url(self, url):
        self.log("正在解析视频地址...")
        try:
            cmd = [self.yt_dlp_path, "--flat-playlist", "--get-id", url]
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=30, encoding="utf-8")
            return True
        except Exception as e:
            return True

    def convert_to_mp4(self, input_file):
        self.log(f"正在转换文件: {input_file}")
        input_ext = os.path.splitext(input_file)[1].lower()
        if input_ext in ('.mp3', '.wav', '.m4a'): return input_file
        output_file = os.path.splitext(input_file)[0] + ".mp4"
        
        ffmpeg_exe = shutil.which(self.ffmpeg_path)
        if not ffmpeg_exe: return input_file
            
        cmd = [ffmpeg_exe, "-hide_banner", "-nostdin", "-i", input_file, "-c:v", "libx264", "-preset", "fast", "-crf", "23", "-c:a", "aac", "-b:a", "192k", "-movflags", "+faststart", "-y", output_file]
        try:
            proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8")
            self.transcode_process = proc
            for line in proc.stdout:
                if self.stop_event.is_set(): break
                if "time=" in line:
                    match = re.search(r'time=(\d+:\d+:\d+\.\d+)', line)
                    if match: self.log(f"转换进度: {match.group(1)}")
            proc.wait()
            if proc.returncode == 0 and os.path.exists(output_file):
                os.remove(input_file)
                return output_file
            return input_file
        except Exception as e:
            return input_file

    def _set_newest_as_last(self, path):
        try:
            files = [os.path.join(path, f) for f in os.listdir(path) if os.path.isfile(os.path.join(path, f))]
            if files:
                self.last_downloaded_file = max(files, key=os.path.getctime)
        except:
            pass

    def do_download(self, url, download_path):
        self.stop_event.clear()
        self.last_downloaded_file = None
        try:
            actual_url = self._extract_url_from_text(url)
            self.log(f"开始处理: {actual_url}")

            if self.is_bilibili_url(actual_url):
                if self.bilibili_download(actual_url, download_path):
                    self._set_newest_as_last(download_path)
                    self.log("所有任务执行完毕")
                    return
                self.log("BBDown 下载失败，尝试使用 yt-dlp...")

            elif self.is_douyin_url(actual_url):
                if self.douyin_download(actual_url, download_path):
                    self._set_newest_as_last(download_path)
                    self.log("所有任务执行完毕")
                    return
                self.log("抖音专用下载失败，尝试使用 yt-dlp...")

            if not self.resolve_url(actual_url):
                return

            cmd = [
                self.yt_dlp_path,
                "-o", os.path.join(download_path, "%(title)s.%(ext)s"),
                "-f", "bv*[ext=mp4]+ba[ext=m4a]/b[ext=mp4]/bv*+ba/b",
                "--ignore-errors", "--no-warnings", "--newline",
                "--concurrent-fragments", "10",
                actual_url
            ]

            ffmpeg_exe = shutil.which(self.ffmpeg_path)
            if ffmpeg_exe: cmd.extend(["--ffmpeg-location", ffmpeg_exe])

            self.log(f"yt-dlp 执行命令: {' '.join(cmd)}")
            self.current_process = subprocess.Popen(
                cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True, encoding="utf-8"
            )
            
            downloaded_paths = set()
            for line in self.current_process.stdout:
                if self.stop_event.is_set(): break
                stripped = line.strip()
                if stripped: 
                    self.log(stripped)
                    if "[download]" in stripped and "%" in stripped:
                        match = re.search(r'\[download\]\s+([\d\.]+%)', stripped)
                        if match:
                            self.log(f"下载进度: {match.group(1)}")
                m = re.match(r'^\[download\]\s+Destination:\s+(.+?)\s*$', stripped) or re.match(r'^\[download\]\s+(.+?)\s+has already been downloaded\s*$', stripped)
                if m: downloaded_paths.add(m.group(1))

            self.current_process.wait()
            
            if self.stop_event.is_set():
                self.log("下载被强制终止")
                return

            downloaded_files = []
            for p in downloaded_paths:
                p = p.strip().strip('"')
                if os.path.isabs(p) and os.path.exists(p):
                    downloaded_files.append(p)
            if not downloaded_files:
                for file in os.listdir(download_path):
                    if file.endswith(('.mp4', '.webm', '.mkv', '.flv', '.m4a')):
                        path = os.path.join(download_path, file)
                        if (time.time() - os.path.getctime(path)) < 300:
                            downloaded_files.append(path)
            
            for file_path in set(downloaded_files):
                if self.stop_event.is_set(): break
                if not file_path.lower().endswith('.mp4'):
                    self.convert_to_mp4(file_path)

            self._set_newest_as_last(download_path)
            self.log("所有任务执行完毕")
        except Exception as e:
            self.log(f"系统错误: {str(e)}")
        finally:
            self.current_process = None
            self.transcode_process = None

downloader = HeadlessDownloader()

# --- Flask Web Server ---

HTML_TEMPLATE = """
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>LinuxYTD - 路由器媒体下载</title>
    <script src="https://cdn.tailwindcss.com"></script>
    <style>
        body { background-color: #0f172a; color: #f8fafc; }
        .log-line { border-bottom: 1px solid #1e293b; padding: 2px 0; }
    </style>
</head>
<body class="min-h-screen flex flex-col items-center justify-center py-10 px-4">
    <div id="main-container" class="w-full max-w-4xl bg-slate-800 rounded-xl shadow-2xl p-8 border border-slate-700 hidden">
        
        <!-- Login View -->
        <div id="login-view" class="space-y-6 hidden w-full max-w-md mx-auto py-10">
            <h1 class="text-3xl font-extrabold text-center text-blue-400 mb-8">LinuxYTD <br><span class="text-sm font-normal text-slate-400">身份验证</span></h1>
            
            <button onclick="login('guest', '')" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg">以游客身份免密极速下载</button>
            
            <div class="relative flex py-5 items-center">
                <div class="flex-grow border-t border-slate-600"></div>
                <span class="flex-shrink-0 mx-4 text-slate-500 text-sm">或</span>
                <div class="flex-grow border-t border-slate-600"></div>
            </div>
            
            <div>
                <input type="password" id="admin-pwd" class="w-full bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 text-slate-200 focus:outline-none focus:border-blue-500" placeholder="输入管理员密钥密码...">
                <button onclick="login('admin', document.getElementById('admin-pwd').value)" class="w-full mt-3 bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg">登录完整版控制台</button>
            </div>
        </div>

        <!-- Guest View -->
        <div id="guest-view" class="hidden space-y-6">
            <div class="flex justify-between items-center mb-4 border-b border-slate-700 pb-4">
                <h2 class="text-2xl font-bold text-emerald-400">游客极速下载器</h2>
                <button onclick="logout()" class="text-sm text-slate-400 hover:text-white px-3 py-1 bg-slate-700 rounded">切换身份</button>
            </div>
            <div>
                <label class="block text-sm font-medium text-slate-400 mb-2">媒体链接 (粘贴即下，自动回传本机并销毁)</label>
                <input type="text" id="guest-url-input" class="w-full bg-slate-900 border border-slate-600 rounded-lg px-4 py-4 text-slate-200 focus:outline-none focus:border-emerald-500 text-lg" placeholder="https://v.douyin.com/...">
            </div>
            <button id="guest-btn-start" onclick="guestStartDownload()" class="w-full bg-emerald-600 hover:bg-emerald-500 text-white font-bold py-4 px-4 rounded-lg transition duration-200 shadow-lg text-lg">开始下载</button>
            
            <div class="mt-6 p-6 bg-slate-900 rounded-lg border border-slate-700">
                <p id="guest-status" class="text-slate-300 text-center text-base font-medium mb-4">等待输入...</p>
                <div class="w-full bg-slate-800 rounded-full h-4 overflow-hidden">
                    <div id="guest-progress" class="bg-emerald-500 h-4 rounded-full transition-all duration-300" style="width: 0%"></div>
                </div>
            </div>
        </div>

        <!-- Admin View -->
        <div id="admin-view" class="hidden">
            <div class="flex items-center justify-between mb-8">
                <h1 class="text-3xl font-extrabold text-blue-400">LinuxYTD <span class="text-sm font-normal text-slate-400">Admin</span></h1>
                <div class="flex space-x-3 items-center">
                    <div id="status-badge" class="px-3 py-1 rounded-full text-sm font-semibold bg-green-900 text-green-300">空闲中</div>
                    <button onclick="logout()" class="text-sm text-slate-400 hover:text-white px-3 py-1 bg-slate-700 rounded">退出</button>
                </div>
            </div>

            <div class="space-y-6">
                <div>
                    <label class="block text-sm font-medium text-slate-400 mb-2">媒体链接</label>
                    <input type="text" id="url-input" class="w-full bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 text-slate-200 focus:outline-none focus:border-blue-500">
                </div>

                <div>
                    <label class="block text-sm font-medium text-slate-400 mb-2">下载保存路径</label>
                    <div class="flex space-x-2">
                        <input type="text" id="path-input" class="flex-1 bg-slate-900 border border-slate-600 rounded-lg px-4 py-3 text-slate-200 focus:outline-none focus:border-blue-500" value="/mnt/sda1/downloads">
                        <button onclick="loadFiles()" class="bg-slate-700 hover:bg-slate-600 text-white px-4 rounded-lg transition border border-slate-600">刷新</button>
                    </div>
                </div>

                <div class="flex space-x-4 pt-2">
                    <button id="btn-start" onclick="adminStartDownload()" class="flex-1 bg-blue-600 hover:bg-blue-500 text-white font-bold py-3 px-4 rounded-lg shadow-lg">提交下载任务</button>
                    <button id="btn-stop" onclick="stopDownload()" class="flex-1 bg-red-600 hover:bg-red-500 text-white font-bold py-3 px-4 rounded-lg shadow-lg disabled:opacity-50" disabled>终止任务</button>
                </div>
            </div>

            <div class="mt-8">
                <h2 class="text-sm font-medium text-slate-400 mb-2">安全管理 (被封禁的公网IP)</h2>
                <div class="bg-slate-900 rounded-lg p-4 border border-slate-700 max-h-40 overflow-y-auto mb-8" id="banned-list">
                    <div class="text-slate-500 text-center text-sm py-2">无封禁记录</div>
                </div>
                
                <h2 class="text-sm font-medium text-slate-400 mb-2">已下载文件管理</h2>
                <div class="bg-slate-900 rounded-lg p-4 border border-slate-700 max-h-60 overflow-y-auto" id="file-list"></div>
            </div>

            <div class="mt-8">
                <div class="flex justify-between items-center mb-2">
                    <h2 class="text-sm font-medium text-slate-400">实时日志控制台</h2>
                    <button onclick="document.getElementById('log-console').innerHTML=''" class="text-xs text-slate-500 hover:text-slate-300">清空</button>
                </div>
                <div id="log-console" class="w-full h-64 overflow-y-auto rounded-lg p-4 text-xs text-green-400 shadow-inner border border-slate-700 bg-black font-mono"></div>
            </div>
        </div>
    </div>

    <!-- Preview Modal -->
    <div id="preview-modal" class="fixed inset-0 bg-slate-900/95 hidden items-center justify-center z-50 backdrop-blur-sm">
        <div class="w-full max-w-5xl p-4 flex flex-col h-full justify-center">
            <div class="flex justify-between items-center mb-4 bg-slate-800 p-4 rounded-lg border border-slate-700">
                <h3 id="preview-title" class="text-white text-lg font-bold truncate pr-4">Preview</h3>
                <button onclick="closePreview()" class="text-slate-400 hover:text-red-400 text-3xl font-bold leading-none">&times;</button>
            </div>
            <div class="bg-black rounded-xl overflow-hidden shadow-2xl flex-1 max-h-[70vh] flex items-center justify-center border border-slate-700">
                <video id="preview-video" controls class="w-full h-full max-h-full outline-none" autoplay name="media"></video>
            </div>
        </div>
    </div>

    <script>
        let currentRole = '';

        async function checkAuth() {
            const res = await fetch('/api/check');
            const data = await res.json();
            currentRole = data.role;
            
            document.getElementById('main-container').classList.remove('hidden');
            document.getElementById('login-view').classList.add('hidden');
            document.getElementById('guest-view').classList.add('hidden');
            document.getElementById('admin-view').classList.add('hidden');
            
            if (currentRole === 'admin') {
                document.getElementById('admin-view').classList.remove('hidden');
                loadFiles();
                loadBannedIPs();
            } else if (currentRole === 'guest') {
                document.getElementById('guest-view').classList.remove('hidden');
            } else {
                document.getElementById('login-view').classList.remove('hidden');
            }
        }

        async function login(role, password) {
            const res = await fetch('/api/login', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ role: role, password: password })
            });
            const data = await res.json();
            if (data.status === 'ok') {
                checkAuth();
            } else {
                alert(data.message || "登录失败");
            }
        }

        async function logout() {
            await fetch('/api/logout', { method: 'POST' });
            checkAuth();
        }

        // SSE Setup
        const source = new EventSource('/api/logs');
        source.onmessage = function(event) {
            if (event.data === "ping") return;
            
            if (currentRole === 'admin') {
                const lc = document.getElementById('log-console');
                const div = document.createElement('div');
                div.className = 'log-line';
                div.textContent = event.data;
                lc.appendChild(div);
                lc.scrollTop = lc.scrollHeight;
            }
            
            if (currentRole === 'guest') {
                const msg = event.data;
                const statusEl = document.getElementById('guest-status');
                const progEl = document.getElementById('guest-progress');
                
                statusEl.textContent = msg.replace(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} - /, '');
                
                const match = msg.match(/进度.*?([\d\.]+)%/);
                if (match) {
                    progEl.style.width = match[1] + '%';
                }
            }

            if (event.data.includes("所有任务执行完毕") || event.data.includes("任务结束") || event.data.includes("被强制终止")) {
                if (currentRole === 'admin') {
                    setAdminStatus(false);
                    loadFiles();
                } else if (currentRole === 'guest') {
                    finishGuestDownload();
                }
            }
        };

        // --- Admin Logic ---
        function setAdminStatus(isRunning) {
            const btnStart = document.getElementById('btn-start');
            const btnStop = document.getElementById('btn-stop');
            const badge = document.getElementById('status-badge');
            
            if (isRunning) {
                btnStart.disabled = true; btnStart.classList.add('opacity-50');
                btnStop.disabled = false; btnStop.classList.remove('opacity-50');
                badge.textContent = '下载中'; badge.className = 'px-3 py-1 rounded-full text-sm font-semibold bg-blue-900 text-blue-300';
            } else {
                btnStart.disabled = false; btnStart.classList.remove('opacity-50');
                btnStop.disabled = true; btnStop.classList.add('opacity-50');
                badge.textContent = '空闲中'; badge.className = 'px-3 py-1 rounded-full text-sm font-semibold bg-green-900 text-green-300';
            }
        }

        async function adminStartDownload() {
            const url = document.getElementById('url-input').value.trim();
            const path = document.getElementById('path-input').value.trim();
            if (!url || !path) return alert("请输入链接和路径");
            
            setAdminStatus(true);
            document.getElementById('log-console').innerHTML = '';
            
            const res = await fetch('/api/start', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ url: url, path: path })
            });
        }

        async function stopDownload() {
            await fetch('/api/stop', { method: 'POST' });
        }

        async function loadBannedIPs() {
            if (currentRole !== 'admin') return;
            const res = await fetch('/api/banned_ips');
            const data = await res.json();
            const container = document.getElementById('banned-list');
            if (data.status !== 'ok') return;
            
            if (data.banned_ips.length === 0) {
                container.innerHTML = '<div class="text-slate-500 text-center text-sm py-2">无封禁记录</div>';
                return;
            }
            
            container.innerHTML = '';
            data.banned_ips.forEach(ip => {
                const div = document.createElement('div');
                div.className = 'flex justify-between items-center py-2 border-b border-slate-700 last:border-0';
                div.innerHTML = `
                    <span class="text-red-400 font-mono text-sm">${ip}</span>
                    <button onclick="unbanIP('${ip}')" class="px-3 py-1 bg-slate-700 hover:bg-slate-600 text-slate-300 rounded text-xs transition">解封</button>
                `;
                container.appendChild(div);
            });
        }
        
        async function unbanIP(ip) {
            await fetch('/api/unban', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ ip: ip })
            });
            loadBannedIPs();
        }

        async function loadFiles() {
            if (currentRole !== 'admin') return;
            const path = document.getElementById('path-input').value.trim();
            const container = document.getElementById('file-list');
            container.innerHTML = '<div class="text-slate-500 text-center py-4">加载中...</div>';
            
            const res = await fetch('/api/files', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ path: path })
            });
            const data = await res.json();
            
            if (data.status !== 'ok') {
                container.innerHTML = `<div class="text-red-400 text-center py-4">${data.message}</div>`;
                return;
            }
            if (data.files.length === 0) {
                container.innerHTML = '<div class="text-slate-500 text-center py-4">目录为空</div>';
                return;
            }
            
            container.innerHTML = '';
            data.files.forEach(f => {
                const sizeMB = (f.size / (1024*1024)).toFixed(2) + ' MB';
                const dlUrl = `/api/download?path=${encodeURIComponent(f.path)}`;
                const streamUrl = `/api/stream?path=${encodeURIComponent(f.path)}`;
                
                const div = document.createElement('div');
                div.className = 'flex items-center justify-between py-3 border-b border-slate-700 hover:bg-slate-800 px-4 rounded';
                div.innerHTML = `
                    <div class="flex flex-col overflow-hidden w-7/12 pr-4">
                        <span class="text-slate-200 text-sm font-medium truncate" title="${f.name}">${f.name}</span>
                        <span class="text-slate-500 text-xs mt-1">${sizeMB}</span>
                    </div>
                    <div class="flex space-x-3 w-5/12 justify-end shrink-0">
                        <button onclick="previewFile('${streamUrl}', '${f.name.replace(/'/g, "\\'")}')" class="px-4 py-1 bg-emerald-600/20 text-emerald-400 hover:bg-emerald-600 hover:text-white rounded shadow text-xs">预览</button>
                        <a href="${dlUrl}" download="${f.name}" class="px-4 py-1 bg-blue-600/20 text-blue-400 hover:bg-blue-600 hover:text-white rounded shadow text-xs">下载</a>
                    </div>
                `;
                container.appendChild(div);
            });
        }

        function previewFile(url, name) {
            document.getElementById('preview-title').textContent = name;
            document.getElementById('preview-video').src = url;
            document.getElementById('preview-modal').classList.remove('hidden');
            document.getElementById('preview-modal').classList.add('flex');
            document.getElementById('preview-video').play().catch(e=>console.log(e));
        }

        function closePreview() {
            const v = document.getElementById('preview-video');
            v.pause(); v.removeAttribute('src'); v.load();
            document.getElementById('preview-modal').classList.add('hidden');
            document.getElementById('preview-modal').classList.remove('flex');
        }

        // --- Guest Logic ---
        async function guestStartDownload() {
            const url = document.getElementById('guest-url-input').value.trim();
            if (!url) return alert("请输入视频链接");
            
            document.getElementById('guest-btn-start').disabled = true;
            document.getElementById('guest-btn-start').classList.add('opacity-50');
            document.getElementById('guest-status').textContent = '准备下载...';
            document.getElementById('guest-progress').style.width = '0%';
            
            await fetch('/api/guest_start', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ url: url })
            });
        }

        async function finishGuestDownload() {
            const statusEl = document.getElementById('guest-status');
            statusEl.textContent = "服务端下载完成！正在拉起浏览器回传...";
            
            const res = await fetch('/api/last_file');
            const data = await res.json();
            
            if (data.path) {
                const dlUrl = `/api/guest_fetch?path=${encodeURIComponent(data.path)}`;
                const a = document.createElement('a');
                a.href = dlUrl;
                a.download = '';
                document.body.appendChild(a);
                a.click();
                document.body.removeChild(a);
                
                statusEl.textContent = "回传指令已发送！下载完成后，路由器上的缓存会自动销毁。";
            } else {
                statusEl.textContent = "回传失败：找不到文件，可能解析失败。";
            }
            
            document.getElementById('guest-btn-start').disabled = false;
            document.getElementById('guest-btn-start').classList.remove('opacity-50');
        }

        window.onload = checkAuth;
    </script>
</body>
</html>
"""

@app.route('/')
def index():
    return render_template_string(HTML_TEMPLATE)

@app.route('/api/login', methods=['POST'])
def api_login():
    ip = get_real_ip()
    if ip in banned_ips:
        return jsonify({"status": "error", "message": "您的IP已被封禁"}), 403

    data = request.json
    role = data.get('role')
    
    if role == 'admin':
        if data.get('password') == 'Ga2011422%':
            session['role'] = 'admin'
            if ip in failed_attempts:
                del failed_attempts[ip]
            return jsonify({"status": "ok"})
            
        if not is_local_ip(ip):
            failed_attempts[ip] = failed_attempts.get(ip, 0) + 1
            if failed_attempts[ip] >= 10:
                banned_ips.add(ip)
                return jsonify({"status": "error", "message": "密码错误连续10次，您的IP已被封禁"}), 403
        
        err_msg = f"密码错误 (失败 {failed_attempts.get(ip, 0)}/10)" if not is_local_ip(ip) else "密码错误"
        return jsonify({"status": "error", "message": err_msg}), 401
        
    elif role == 'guest':
        session['role'] = 'guest'
        return jsonify({"status": "ok"})
    return jsonify({"status": "error"}), 400

@app.route('/api/banned_ips', methods=['GET'])
def get_banned_ips():
    if session.get('role') != 'admin': return "Unauthorized", 401
    return jsonify({"status": "ok", "banned_ips": list(banned_ips)})

@app.route('/api/unban', methods=['POST'])
def unban_ip():
    if session.get('role') != 'admin': return "Unauthorized", 401
    ip = request.json.get('ip')
    if ip in banned_ips:
        banned_ips.remove(ip)
    if ip in failed_attempts:
        del failed_attempts[ip]
    return jsonify({"status": "ok"})

@app.route('/api/check')
def api_check():
    return jsonify({"role": session.get('role', '')})

@app.route('/api/logout', methods=['POST'])
def api_logout():
    session.clear()
    return jsonify({"status": "ok"})

@app.route('/api/start', methods=['POST'])
def start_task():
    if session.get('role') != 'admin': return "Unauthorized", 401
    data = request.json
    url = data.get('url')
    path = data.get('path')
    if not os.path.exists(path):
        try: os.makedirs(path, exist_ok=True)
        except: pass
    threading.Thread(target=downloader.do_download, args=(url, path), daemon=True).start()
    return jsonify({"status": "ok"})

@app.route('/api/guest_start', methods=['POST'])
def guest_start():
    if session.get('role') != 'guest': return "Unauthorized", 401
    url = request.json.get('url')
    os.makedirs(GUEST_DIR, exist_ok=True)
    threading.Thread(target=downloader.do_download, args=(url, GUEST_DIR), daemon=True).start()
    return jsonify({"status": "ok"})

@app.route('/api/stop', methods=['POST'])
def stop_task():
    if session.get('role') != 'admin': return "Unauthorized", 401
    downloader.stop_download()
    return jsonify({"status": "ok"})

@app.route('/api/files', methods=['POST'])
def list_files():
    if session.get('role') != 'admin': return "Unauthorized", 401
    path = request.json.get('path')
    if not path or not os.path.exists(path) or not os.path.isdir(path):
        return jsonify({"status": "error", "message": "目录不存在"}), 400
    
    files = []
    try:
        for f in os.listdir(path):
            if f.lower().endswith(('.mp4', '.webm', '.mkv', '.flv', '.avi', '.mp3', '.wav', '.m4a')):
                fp = os.path.join(path, f)
                if os.path.isfile(fp):
                    files.append({"name": f, "size": os.path.getsize(fp), "path": fp})
        files.sort(key=lambda x: os.path.getmtime(x["path"]), reverse=True)
    except Exception as e:
        return jsonify({"status": "error", "message": str(e)}), 500
    return jsonify({"status": "ok", "files": files})

@app.route('/api/download')
def download_api():
    if session.get('role') != 'admin': return "Unauthorized", 401
    filepath = request.args.get('path')
    if not filepath or not os.path.exists(filepath): return "Not found", 404
    return send_from_directory(os.path.dirname(filepath), os.path.basename(filepath), as_attachment=True)

@app.route('/api/stream')
def stream_api():
    if session.get('role') != 'admin': return "Unauthorized", 401
    filepath = request.args.get('path')
    if not filepath or not os.path.exists(filepath): return "Not found", 404
    return send_from_directory(os.path.dirname(filepath), os.path.basename(filepath), as_attachment=False)

@app.route('/api/last_file')
def get_last_file():
    if session.get('role') != 'guest': return "Unauthorized", 401
    return jsonify({"path": downloader.last_downloaded_file})

@app.route('/api/guest_fetch')
def guest_fetch():
    if session.get('role') != 'guest': return "Unauthorized", 401
    filepath = request.args.get('path')
    # 安全检查，防止路径穿越
    if not filepath or not filepath.startswith(GUEST_DIR) or not os.path.exists(filepath):
        return "Not found", 404
        
    return send_from_directory(os.path.dirname(filepath), os.path.basename(filepath), as_attachment=True)

@app.route('/api/logs')
def stream_logs():
    def generate():
        while True:
            try:
                msg = downloader.log_queue.get(timeout=15)
                yield f"data: {msg}\n\n"
            except queue.Empty:
                yield "data: ping\n\n"
    return Response(generate(), mimetype='text/event-stream')

if __name__ == '__main__':
    print("Starting LinuxYTD Web Server on port 8523...")
    app.run(host='::', port=8523, debug=False, threaded=True)
