#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import os
import re
import json
import time
import zipfile
import shutil
import warnings
import urllib.parse
import ssl
from datetime import datetime, timedelta
from urllib.parse import unquote
from typing import List, Dict, Tuple, Optional
import requests
from xml.etree import ElementTree as ET
from tqdm import tqdm
from telethon.sync import TelegramClient
from telethon.tl.types import DocumentAttributeFilename
from requests.adapters import HTTPAdapter
from urllib3.poolmanager import PoolManager

class CustomHTTPAdapter(HTTPAdapter):
    def init_poolmanager(self, *args, **kwargs):
        kwargs['ssl_context'] = self.create_ssl_context()
        return super().init_poolmanager(*args, **kwargs)

    def create_ssl_context(self):
        ctx = ssl.create_default_context()
        ctx.check_hostname = False
        ctx.verify_mode = ssl.CERT_NONE
        ctx.set_ciphers('DEFAULT@SECLEVEL=1')
        return ctx

requests.packages.urllib3.disable_warnings()
ssl._create_default_https_context = ssl._create_unverified_context
warnings.filterwarnings("ignore", category=requests.packages.urllib3.exceptions.InsecureRequestWarning)

WEBHOOK_URL = "https://qyapi.weixin.qq.com/cgi-bin/webhook/send?key=c22a41d4-dc30-49ab-a2ae-e9fdb3617474"
TIMEOUT = 60
RETRY_TIMES = 3
SIZE_TOLERANCE = 20480
TIME_TOLERANCE = 60
DATE_FORMAT = "%Y-%m-%d"
DATE_FORMAT_FILE = "%Y%m%d"
TIME_FORMAT = "%Y-%m-%d %H:%M"
TIME_FORMAT_WITH_SEC = "%Y-%m-%d %H:%M:%S"
MIN_VALID_TIMESTAMP = time.mktime(datetime(1970, 1, 1).timetuple())
MAX_VALID_TIMESTAMP = time.mktime(datetime(2030, 12, 31).timetuple())
LOCAL_API_BASE = "https://lz.qaiu.top"
PUBLIC_API_BASE = "https://lz.qaiu.top"
GITHUB_PROXY_LIST = [
    "https://gh-proxy.com//https://raw.githubusercontent.com",
    "https://hub.gitmirror.com/https://raw.githubusercontent.com",
    "https://gh.api.99988866.xyz/https://raw.githubusercontent.com",
]
DOWNLOAD_CHUNK_SIZE = 8192
MIN_VALID_FILE_SIZE = 10240

KU9_TARGET_URL = f"{LOCAL_API_BASE}/v2/getFileList?url=https://wfy.lanzout.com/b0izqfjwb&pwd=123456"
YYGJ_TARGET_URL = f"{LOCAL_API_BASE}/v2/getFileList?url=https://share.feijipan.com/s/ZHVcIJIu&pwd="
KU9_SAVE_DIR = "/mnt/sda1/webdav/酷九电视"
YYGJ_SAVE_DIR = "/mnt/sda1/webdav/应用管家"
YYGJ_TARGET_DIR_KEYWORD = "应用管家"
YYGJ_TARGET_FILE = "普通版.apk"

OK_WEBDAV_BASE_URL = "http://127.0.0.1/alist/dav/quark"
OK_USERNAME = "admin"
OK_PASSWORD = "admin"
OK_REMOTE_DIRS = ["欧克增强", "欧克标准"]
OK_MERGED_DIR = "/mnt/sda1/webdav/欧克影视"
OK_MAX_RETRIES = 3
OK_MAX_FILES_TO_SYNC = 2

TANGSHI_GITHUB_REPO_OWNER = "FongMi"
TANGSHI_GITHUB_REPO_NAME = "Release"
TANGSHI_GITHUB_BRANCH = "fongmi"
TANGSHI_APK_PATH = "apk"
TANGSHI_DOWNLOAD_DIR = "/mnt/sda1/webdav/唐三影视"

MYDV_GITHUB_REPO_OWNER = "mytv-android"
MYDV_GITHUB_REPO_NAME = "myDV"
MYDV_LITE_REPO_OWNER = "mytv-android"
MYDV_LITE_REPO_NAME = "mydv-Lite"
MYDV_GITHUB_TOKEN = "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"
MYDV_DOWNLOAD_DIR = "/mnt/sda1/webdav/电视抖音"

BLBL_GITHUB_REPO_OWNER = "cat3399"
BLBL_GITHUB_REPO_NAME = "blbl"
BLBL_DOWNLOAD_DIR = "/mnt/sda1/webdav/哔哩哔哩"
BLBL_FILE_PATTERN = r"app-release\.apk"

MOYU_GITHUB_REPO_OWNER = "sfee1212"
MOYU_GITHUB_REPO_NAME = "moyutv"
MOYU_DOWNLOAD_DIR = "/mnt/sda1/webdav/摸鱼电视"

FISH_GITHUB_REPO_OWNER = "fish2018"
FISH_GITHUB_REPO_NAME = "webhtv"
FISH_DOWNLOAD_DIR = "/mnt/sda1/webdav/鱼佬影视"

XIAOSA_GITHUB_REPO_OWNER = "PizazzGY"
XIAOSA_GITHUB_REPO_NAME = "NewTVBox"
XIAOSA_FILE_PATH = "local/单线路.zip"
XIAOSA_DOWNLOAD_DIR = "/mnt/sda1/webdav/本地影包/"
XIAOSA_EXTRACT_DIR = "/mnt/sda1/html/pg/xiaosa"

TG_API_ID = '21632167'
TG_API_HASH = '5d0ae2cd185f76adafdd5cb5943b645c'
TG_SESSION_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'tg.session')
KU9_CACHE_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'ku9_time.json')

def print_section(title: str):
    print(f"{'='*70}\n📌 开始执行：{title}\n{'='*70}")

def print_line():
    print('='*70)

class SyncUtils:
    @staticmethod
    def ensure_dir(path):
        os.makedirs(path, exist_ok=True)

    @staticmethod
    def is_file_current(filepath, expected_timestamp, tolerance=TIME_TOLERANCE):
        if not os.path.exists(filepath):
            return False
        if expected_timestamp is None:
            return True
        local_mtime = os.path.getmtime(filepath)
        if expected_timestamp <= local_mtime:
            return True
        if abs(local_mtime - expected_timestamp) <= tolerance:
            return True
        return False

    @staticmethod
    def clean_old_files(directory, keep_filenames=None, pattern=None, min_size=MIN_VALID_FILE_SIZE, safe_mode=True):
        if not os.path.exists(directory):
            return 0
        if safe_mode and (not keep_filenames or len(keep_filenames) == 0):
            return 0
        keep_set = set(keep_filenames) if keep_filenames else set()
        deleted = 0
        for filename in os.listdir(directory):
            if filename in keep_set:
                continue
            if pattern and not re.match(pattern, filename):
                continue
            filepath = os.path.join(directory, filename)
            if os.path.isfile(filepath) and os.path.getsize(filepath) > min_size:
                try:
                    os.remove(filepath)
                    deleted += 1
                except Exception:
                    pass
        return deleted

    @staticmethod
    def validate_api_response(data, context=""):
        if data.get("code") != 200 or not data.get("success"):
            raise ValueError(f"{context}请求失败：code={data.get('code')}, msg={data.get('msg')}")
        return data.get("data", [])

    @staticmethod
    def get_proxy_urls(original_url, replace_patterns=None):
        url = original_url
        if replace_patterns:
            for pattern in replace_patterns:
                url = url.replace(pattern, "")
        if not url.startswith("/"):
            url = "/" + url
        return [f"{proxy}{url}" for proxy in GITHUB_PROXY_LIST] + [original_url]

    @staticmethod
    def safe_remove_temp(temp_path):
        if os.path.exists(temp_path):
            try:
                os.remove(temp_path)
            except Exception:
                pass

utils = SyncUtils()

class TaskStats:
    def __init__(self, total_tasks):
        self.stats = {
            "total_tasks": total_tasks,
            "completed_tasks": 0,
            "failed_tasks": 0,
            "has_updates": False
        }
        self.task_data = {}

    def update(self, task_key, task_name, stats_data):
        has_update = stats_data.get("success_count", 0) > 0
        self.task_data[task_key] = {
            "status": "completed",
            "data": {"task_name": task_name, **stats_data},
            "has_update": has_update
        }
        self.stats["completed_tasks"] += 1
        if has_update:
            self.stats["has_updates"] = True

    def get_task(self, task_key):
        return self.task_data.get(task_key, {"has_update": False, "data": {}})

    def has_update(self):
        return self.stats["has_updates"]

    def print_summary(self, stats):
        summary = f"📊 执行完成："
        if stats.get("total_files") is not None:
            summary += f"总文件数{stats['total_files']} "
        summary += f"✅ 成功{stats['success_count']} "
        if stats.get("skip_count", 0) > 0:
            summary += f"⏩ 跳过{stats['skip_count']} "
        summary += f"🧹 删除{stats.get('deleted_count', 0)} "
        if stats.get("fail_count", 0) > 0:
            summary += f"❌ 失败{stats['fail_count']} "
        print(summary.strip())

task_stats = TaskStats(11)

def finalize_download(temp_path, save_path, timestamp):
    if os.path.exists(save_path):
        os.remove(save_path)
    os.rename(temp_path, save_path)
    os.utime(save_path, (timestamp, timestamp))
    return {
        'name': os.path.basename(save_path),
        'size': os.path.getsize(save_path),
        'timestamp': timestamp,
        'save_path': save_path
    }

def download_file_with_retry(
    url: str,
    save_path: str,
    expected_size: int = None,
    expected_timestamp: float = None,
    auth: Tuple[str, str] = None,
    headers: dict = None,
    retries: int = RETRY_TIMES,
    timeout: int = TIMEOUT,
    chunk_size: int = DOWNLOAD_CHUNK_SIZE,
    verify: bool = False,
    proxy_urls: List[str] = None,
    set_timestamp: bool = True
) -> Tuple[bool, Optional[dict]]:
    if utils.is_file_current(save_path, expected_timestamp):
        print(f"⏩ 文件已最新，跳过下载：{os.path.basename(save_path)}")
        return True, None
    urls_to_try = proxy_urls if proxy_urls else [url]
    temp_path = save_path + ".tmp"
    os.makedirs(os.path.dirname(save_path), exist_ok=True)
    for attempt_idx, current_url in enumerate(urls_to_try):
        try:
            resp = requests.get(current_url, stream=True, timeout=timeout, auth=auth, headers=headers, verify=verify)
            resp.raise_for_status()
            actual_size = int(resp.headers.get('Content-Length', 0))
            with open(temp_path, 'wb') as f, tqdm(total=actual_size if actual_size else None, unit='B', unit_scale=True, desc=f"下载 {os.path.basename(save_path)}", leave=False) as pbar:
                for chunk in resp.iter_content(chunk_size=chunk_size):
                    if chunk:
                        f.write(chunk)
                        pbar.update(len(chunk))
                        f.flush()
                        os.fsync(f.fileno())
            local_size = os.path.getsize(temp_path)
            if actual_size and abs(local_size - actual_size) > SIZE_TOLERANCE:
                raise ValueError(f"文件大小不匹配")
            timestamp = expected_timestamp
            if timestamp is None:
                last_modified = resp.headers.get('Last-Modified')
                if last_modified:
                    try:
                        dt = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z")
                        timestamp = dt.timestamp()
                    except:
                        timestamp = time.time()
                else:
                    timestamp = time.time()

            if set_timestamp:
                file_info = finalize_download(temp_path, save_path, timestamp)
            else:
                if os.path.exists(save_path):
                    os.remove(save_path)
                os.rename(temp_path, save_path)
                file_info = {
                    'name': os.path.basename(save_path),
                    'size': os.path.getsize(save_path),
                    'timestamp': timestamp,
                    'save_path': save_path
                }

            print(f"✅ 下载完成：{os.path.basename(save_path)}")
            return True, file_info
        except Exception:
            utils.safe_remove_temp(temp_path)
            if attempt_idx == len(urls_to_try) - 1:
                return False, None
            time.sleep(2)
    return False, None

def perform_downloads(
    task_name: str,
    file_infos: List[dict],
    download_dir: str,
    delete_old: bool = True,
    file_pattern: str = None,
    set_timestamp: bool = True,
    **default_download_kwargs
) -> dict:
    success_count = 0
    skip_count = 0
    fail_count = 0
    downloaded_files = []
    expected_names = [fi["name"] for fi in file_infos]
    for file_info in file_infos:
        save_path = os.path.join(download_dir, file_info["name"])
        extra_kwargs = file_info.get("extra_download_kwargs", {})
        download_kwargs = {**default_download_kwargs, **extra_kwargs}
        use_timestamp = file_info.get("set_timestamp", set_timestamp)
        success, info = download_file_with_retry(
            url=file_info["url"],
            save_path=save_path,
            expected_size=file_info.get("size"),
            expected_timestamp=file_info.get("timestamp"),
            set_timestamp=use_timestamp,
            **download_kwargs
        )
        if success:
            if info:
                success_count += 1
                downloaded_files.append(info)
            else:
                skip_count += 1
        else:
            fail_count += 1
    deleted_count = 0
    if delete_old and expected_names:
        deleted_count = utils.clean_old_files(download_dir, keep_filenames=expected_names, pattern=file_pattern, safe_mode=True)
    stats = {
        "total_files": len(file_infos),
        "success_count": success_count,
        "skip_count": skip_count,
        "fail_count": fail_count,
        "deleted_count": deleted_count,
        "save_path": download_dir,
        "downloaded_files": downloaded_files
    }
    task_stats.print_summary(stats)
    return stats

def handle_errors(func):
    def wrapper(*args, **kwargs):
        try:
            return func(*args, **kwargs)
        except Exception as e:
            print(f"❌ {func.__name__} 失败: {e}")
            return None
    return wrapper

def load_ku9_cache() -> Dict[str, dict]:
    if os.path.exists(KU9_CACHE_FILE):
        try:
            with open(KU9_CACHE_FILE, 'r', encoding='utf-8') as f:
                return json.load(f)
        except:
            return {}
    return {}

def save_ku9_cache(cache: Dict[str, dict]):
    try:
        utils.ensure_dir(os.path.dirname(KU9_CACHE_FILE))
        with open(KU9_CACHE_FILE, 'w', encoding='utf-8') as f:
            json.dump(cache, f, ensure_ascii=False, indent=2)
    except:
        pass

def _parse_create_time_raw(create_time_str: str) -> Optional[float]:
    if not create_time_str:
        return None
    try:
        now = datetime.now()

        if ":" in create_time_str and any(char.isdigit() for char in create_time_str[:10]):
            for fmt in [TIME_FORMAT_WITH_SEC, TIME_FORMAT]:
                try:
                    dt = datetime.strptime(create_time_str.strip(), fmt)
                    return time.mktime(dt.timetuple())
                except ValueError:
                    continue

        if re.match(r'\d{4}-\d{2}-\d{2}', create_time_str):
            dt = datetime.strptime(create_time_str.strip(), "%Y-%m-%d")
            return time.mktime(dt.timetuple())

        time_mappings = [("前天", lambda: now - timedelta(days=2)), ("昨天", lambda: now - timedelta(days=1))]
        for prefix, delta_func in time_mappings:
            if create_time_str.startswith(prefix):
                date = delta_func()
                time_str = create_time_str.replace(prefix, "").strip()
                full_time = f"{date.strftime(DATE_FORMAT)} {time_str if time_str else '23:59'}"
                return time.mktime(datetime.strptime(full_time, TIME_FORMAT).timetuple())

        for unit, seconds in [("天前", 86400), ("小时前", 3600), ("分钟前", 60)]:
            if unit in create_time_str:
                match = re.search(r'(\d+(?:\.\d+)?)\s*' + unit, create_time_str)
                if match:
                    value = float(match.group(1))
                    target_time = now - timedelta(seconds=value * seconds)
                    if unit == "天前":
                        full_time = f"{target_time.strftime(DATE_FORMAT)} 23:59"
                        return time.mktime(datetime.strptime(full_time, TIME_FORMAT).timetuple())
                    return time.mktime(target_time.timetuple())

        dt = datetime.strptime(create_time_str.strip(), DATE_FORMAT)
        return time.mktime(dt.timetuple())
    except:
        return None

def parse_create_time_with_cache(create_time_str: str, file_id: str = None, cache: Dict[str, dict] = None) -> Optional[float]:
    if not file_id or not cache:
        return _parse_create_time_raw(create_time_str)

    cached_entry = cache.get(file_id)
    if cached_entry and cached_entry.get("original_time") == create_time_str:
        remote_ts = cached_entry.get("remote_timestamp")
        if remote_ts is not None:
            return remote_ts

    remote_ts = _parse_create_time_raw(create_time_str)
    if remote_ts is not None and file_id and cache:
        if file_id not in cache:
            cache[file_id] = {}
        cache[file_id]["remote_timestamp"] = remote_ts
        cache[file_id]["original_time"] = create_time_str
    return remote_ts

def tg_extract_zip(zp, ex, is_chinese=False):
    try:
        utils.ensure_dir(ex)
        with zipfile.ZipFile(zp) as z:
            file_count = len(z.infolist())
            for m in z.infolist():
                if is_chinese:
                    try:
                        m.filename = m.filename.encode('cp437').decode('gbk')
                    except:
                        pass
                tp = os.path.join(ex, m.filename)
                os.makedirs(os.path.dirname(tp), exist_ok=True)
                z.extract(m, ex)
                os.utime(tp, (time.mktime(datetime(*m.date_time).timetuple()),)*2)
        return file_count
    except:
        return 0
        
@handle_errors
def run_ku9():
    task_name = "酷九软件下载"
    print_section(task_name)
    cache = load_ku9_cache()
    max_retries = 5
    response = None
    for attempt in range(max_retries):
        try:
            response = requests.get(KU9_TARGET_URL, timeout=TIMEOUT, verify=False)
            response.raise_for_status()
            break
        except Exception:
            if attempt == max_retries - 1:
                raise
            time.sleep(3)
    file_list = utils.validate_api_response(response.json(), "酷九")
    need_download = []
    keep_names = set()
    cache_updated = False

    for item in file_list:
        file_url = item.get("parserUrl")
        original_name = item.get("fileName")
        if not file_url or not original_name:
            continue

        raw_create_time = item.get("createTime")
        remote_timestamp = parse_create_time_with_cache(raw_create_time, original_name, cache)
        if remote_timestamp is None:
            continue

        # 获取上次成功下载的时间（默认为0）
        cache_entry = cache.get(original_name, {})
        last_check_time = cache_entry.get("last_check_time", 0)

        # 决策：只有远程时间戳 > 上次检查时间才下载
        if remote_timestamp > last_check_time:
            # 生成带日期前缀的新文件名
            date_prefix = datetime.fromtimestamp(remote_timestamp).strftime(DATE_FORMAT_FILE) + "_"
            name_without_ext, ext = os.path.splitext(original_name)
            new_name = f"{date_prefix}{name_without_ext}{ext}"
            now_ts = time.time()

            need_download.append({
                "name": new_name,
                "url": file_url.replace(LOCAL_API_BASE, PUBLIC_API_BASE),
                "size": item.get("size", 0),
                "timestamp": remote_timestamp,
                "set_timestamp": False,
                "original_name": original_name,
                "raw_create_time": raw_create_time,
                "new_check_time": now_ts,
                "remote_timestamp": remote_timestamp
            })
        else:
            # 不需要下载，但需要保留文件名用于清理旧文件
            date_prefix = datetime.fromtimestamp(remote_timestamp).strftime(DATE_FORMAT_FILE) + "_"
            name_without_ext, ext = os.path.splitext(original_name)
            new_name = f"{date_prefix}{name_without_ext}{ext}"
            keep_names.add(new_name)
            print(f"⏩ 文件已最新，跳过下载：{new_name}")

    # 执行下载（和之前类似，但需要更新缓存中的 last_check_time）
    success_count = 0
    skip_count = 0
    fail_count = 0
    downloaded_files = []

    for file_info in need_download:
        save_path = os.path.join(KU9_SAVE_DIR, file_info["name"])
        original_name = file_info.pop("original_name", None)
        raw_create_time = file_info.pop("raw_create_time", None)
        new_check_time = file_info.pop("new_check_time", None)
        remote_ts = file_info.pop("remote_timestamp", None)

        success, info = download_file_with_retry(
            url=file_info["url"],
            save_path=save_path,
            expected_size=file_info.get("size"),
            expected_timestamp=file_info.get("timestamp"),
            set_timestamp=file_info.get("set_timestamp", False),
            retries=RETRY_TIMES,
            timeout=TIMEOUT
        )
        if success and info:
            success_count += 1
            downloaded_files.append(info)
            keep_names.add(file_info["name"])
            # 更新缓存中的 last_check_time 和 remote_timestamp
            if original_name and new_check_time:
                if original_name not in cache:
                    cache[original_name] = {}
                cache[original_name]["last_check_time"] = new_check_time
                cache[original_name]["original_time"] = raw_create_time
                cache[original_name]["remote_timestamp"] = remote_ts
                cache[original_name]["file_name"] = original_name
                cache[original_name]["last_updated"] = datetime.now().strftime(TIME_FORMAT_WITH_SEC)
                cache_updated = True
        elif success and info is None:
            skip_count += 1
            keep_names.add(file_info["name"])
        else:
            fail_count += 1
            if os.path.exists(save_path):
                keep_names.add(file_info["name"])

    # 删除旧文件
    deleted_count = 0
    if keep_names:
        deleted_count = utils.clean_old_files(
            KU9_SAVE_DIR,
            keep_filenames=list(keep_names),
            pattern=r".*",
            safe_mode=True
        )

    if cache_updated:
        save_ku9_cache(cache)

    stats = {
        "total_files": len(file_list),
        "success_count": success_count,
        "skip_count": skip_count,
        "fail_count": fail_count,
        "deleted_count": deleted_count,
        "save_path": KU9_SAVE_DIR,
        "downloaded_files": downloaded_files
    }
    task_stats.print_summary(stats)
    task_stats.update("ku9", task_name, stats)
    
@handle_errors
def run_yygj():
    task_name = "应用管家下载"
    print_section(task_name)

    max_retries = 5
    response = None
    for attempt in range(max_retries):
        try:
            response = requests.get(YYGJ_TARGET_URL, timeout=TIMEOUT, verify=False)
            response.raise_for_status()
            break
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(3)

    file_list = utils.validate_api_response(response.json(), "应用管家")
    target_dirs = []

    for item in file_list:
        file_name = item.get("fileName", "")
        file_type = item.get("fileType", item.get("type", ""))
        is_dir = file_type in ["folder", "dir", "directory"] or ("/" in file_name and item.get("size", 0) == 0)
        if is_dir and YYGJ_TARGET_DIR_KEYWORD in file_name:
            timestamp = parse_create_time_with_cache(item.get("createTime"))
            if timestamp is not None:
                target_dirs.append((item, timestamp))

    if not target_dirs:
        print(f"⚠️ 未找到包含 '{YYGJ_TARGET_DIR_KEYWORD}' 的目录")
        return

    latest_dir, _ = max(target_dirs, key=lambda x: x[1])
    latest_dir_name = latest_dir.get("fileName")
    latest_dir_url = latest_dir.get("parserUrl", "").replace(LOCAL_API_BASE, PUBLIC_API_BASE)
    print(f"🔍 正在获取【{latest_dir_name}】目录下的文件列表")

    dir_response = None
    for attempt in range(max_retries):
        try:
            dir_response = requests.get(latest_dir_url, timeout=TIMEOUT, verify=False)
            dir_response.raise_for_status()
            break
        except Exception as e:
            if attempt == max_retries - 1:
                raise
            time.sleep(3)

    dir_file_list = utils.validate_api_response(dir_response.json(), f"目录{latest_dir_name}")

    file_info = None
    for item in dir_file_list:
        if item.get("fileName", "").strip() == YYGJ_TARGET_FILE:
            file_url = item.get("parserUrl", "").replace(LOCAL_API_BASE, PUBLIC_API_BASE)
            file_timestamp = parse_create_time_with_cache(item.get("createTime"))
            file_info = {
                "name": YYGJ_TARGET_FILE,
                "url": file_url,
                "size": item.get("size", 0),
                "timestamp": file_timestamp,
                "dir_name": latest_dir_name
            }
            break

    if not file_info:
        print(f"⚠️ 在目录 {latest_dir_name} 中未找到文件 '{YYGJ_TARGET_FILE}'")
        return

    version_match = re.search(r'v(\d+(?:\.\d+)+)', latest_dir_name)
    if version_match:
        version = version_match.group(1)
        file_info["name"] = f"普通版{version}.apk"
    else:
        ts = file_info.get("timestamp", time.time())
        file_info["name"] = f"普通版_{int(ts)}.apk"

    print(f"✅ 成功获取文件：普通版.apk（{file_info['size']/1024/1024:.2f}MB） -> 重命名为 {file_info['name']}")
    stats = perform_downloads(task_name, [file_info], YYGJ_SAVE_DIR, delete_old=True, file_pattern=r".*\.apk$")
    if stats["downloaded_files"]:
        stats["downloaded_file"] = stats["downloaded_files"][0]
        stats["downloaded_file"]["dir_name"] = file_info["dir_name"]
    task_stats.update("yygj", task_name, stats)

@handle_errors
def run_mydv():
    task_name = "电视抖音下载"
    print_section(task_name)
    api_url = f"https://api.github.com/repos/{MYDV_GITHUB_REPO_OWNER}/{MYDV_GITHUB_REPO_NAME}/releases/latest"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    response = session.get(api_url, headers=headers, timeout=30)
    response.raise_for_status()
    release_data = response.json()
    apk_assets = [asset for asset in release_data.get("assets", []) if asset["name"].lower().endswith(".apk")]
    if not apk_assets:
        return
    asset = apk_assets[0]
    url = asset["browser_download_url"]
    size = asset["size"]
    time_str = asset.get("updated_at") or asset.get("created_at")
    timestamp = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").timestamp() if time_str else None
    original_name = url.split("/")[-1]
    tag_name = release_data.get("tag_name", "")
    version_match = re.search(r'(\d+(?:\.\d+)+)', tag_name)
    if not version_match:
        version_match = re.search(r'(\d+(?:\.\d+)+)', original_name)
    if version_match:
        version = version_match.group(1)
        file_name = f"电视抖音{version}.apk"
    else:
        file_name = f"电视抖音_{int(timestamp or time.time())}.apk"
    print(f"✅ 成功获取文件：app-release.apk -> 重命名为 {file_name}（{size/1024/1024:.2f}MB）")
    proxy_urls = [url] + utils.get_proxy_urls(url, ["https://github.com"])
    file_info = {
        "name": file_name,
        "url": url,
        "size": size,
        "timestamp": timestamp,
        "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
    }
    stats = perform_downloads(task_name, [file_info], MYDV_DOWNLOAD_DIR, delete_old=True, file_pattern=r"电视抖音.*\.apk")
    if stats["downloaded_files"]:
        stats["downloaded_file"] = stats["downloaded_files"][0]
    task_stats.update("mydv", task_name, stats)

@handle_errors
def run_mydv_lite():
    task_name = "迷你抖音下载"
    print_section(task_name)
    api_url = f"https://api.github.com/repos/{MYDV_LITE_REPO_OWNER}/{MYDV_LITE_REPO_NAME}/releases/latest"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    try:
        response = session.get(api_url, headers=headers, timeout=30)
        response.raise_for_status()
        release_data = response.json()
    except Exception:
        return
    apk_assets = [asset for asset in release_data.get("assets", []) if asset["name"].lower().endswith(".apk")]
    if not apk_assets:
        return
    asset = apk_assets[0]
    url = asset["browser_download_url"]
    size = asset["size"]
    time_str = asset.get("updated_at") or asset.get("created_at") or release_data.get("published_at")
    timestamp = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").timestamp() if time_str else None
    tag_name = release_data.get("tag_name", "")
    version_match = re.search(r'(\d+(?:\.\d+)+)', tag_name)
    if version_match:
        version = version_match.group(1)
    else:
        version = tag_name
    if version:
        final_name = f"迷你抖音{version}.apk"
    else:
        final_name = f"迷你抖音_{int(timestamp or time.time())}.apk"
    original_asset_name = asset["name"]
    print(f"✅ 成功获取文件：{original_asset_name} -> 重命名为 {final_name}（{size/1024/1024:.2f}MB）")
    proxy_urls = [url] + utils.get_proxy_urls(url, ["https://github.com"])
    file_info = {
        "name": final_name,
        "url": url,
        "size": size,
        "timestamp": timestamp,
        "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
    }
    stats = perform_downloads(task_name, [file_info], MYDV_DOWNLOAD_DIR, delete_old=True, file_pattern=r"迷你抖音.*\.apk")
    if stats.get("downloaded_files"):
        stats["downloaded_file"] = stats["downloaded_files"][0]
    task_stats.update("mydv_lite", task_name, stats)

@handle_errors
def run_blbl():
    task_name = "哔哩哔哩下载"
    print_section(task_name)
    api_url = f"https://api.github.com/repos/{BLBL_GITHUB_REPO_OWNER}/{BLBL_GITHUB_REPO_NAME}/releases/latest"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    response = session.get(api_url, headers=headers, timeout=30)
    response.raise_for_status()
    release_data = response.json()
    apk_assets = [asset for asset in release_data.get("assets", []) if re.match(BLBL_FILE_PATTERN, asset["name"])]
    if not apk_assets:
        return
    asset = apk_assets[0]
    url = asset["browser_download_url"]
    size = asset["size"]
    time_str = asset.get("updated_at") or asset.get("created_at")
    timestamp = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").timestamp() if time_str else None
    original_name = asset["name"]
    tag_name = release_data.get("tag_name", "")
    version_match = re.search(r'(\d+(?:\.\d+)+)', tag_name)
    if not version_match:
        version_match = re.search(r'(\d+(?:\.\d+)+)', original_name)
    if version_match:
        version = version_match.group(1)
        file_name = f"哔哩哔哩{version}.apk"
    else:
        file_name = f"哔哩哔哩_{int(timestamp or time.time())}.apk"
    print(f"✅ 成功获取文件：app-release.apk -> 重命名为 {file_name}（{size/1024/1024:.2f}MB）")
    proxy_urls = [url] + utils.get_proxy_urls(url, ["https://github.com"])
    file_info = {
        "name": file_name,
        "url": url,
        "size": size,
        "timestamp": timestamp,
        "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
    }
    stats = perform_downloads(task_name, [file_info], BLBL_DOWNLOAD_DIR, delete_old=True, file_pattern=r".*\.apk$")
    if stats["downloaded_files"]:
        stats["downloaded_file"] = stats["downloaded_files"][0]
    task_stats.update("blbl", task_name, stats)

@handle_errors
def run_moyu():
    task_name = "摸鱼电视下载"
    print_section(task_name)

    api_url = f"https://api.github.com/repos/{MOYU_GITHUB_REPO_OWNER}/{MOYU_GITHUB_REPO_NAME}/releases/latest"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}

    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False

    try:
        response = session.get(api_url, headers=headers, timeout=30)
        response.raise_for_status()
        release_data = response.json()
    except Exception as e:
        print(f"❌ 获取 Release 信息失败: {e}")
        return

    apk_assets = [asset for asset in release_data.get("assets", []) if asset["name"].lower().endswith(".apk")]
    if not apk_assets:
        print("⚠️ 未在 Release 中找到 APK 文件")
        return

    asset = apk_assets[0]
    url = asset["browser_download_url"]
    size = asset["size"]
    time_str = asset.get("updated_at") or asset.get("created_at")
    timestamp = datetime.strptime(time_str, "%Y-%m-%dT%H:%M:%SZ").timestamp() if time_str else None

    tag_name = release_data.get("tag_name", "")
    version_match = re.search(r'(\d+(?:\.\d+)+)', tag_name)
    if version_match:
        version = version_match.group(1)
        file_name = f"摸鱼电视{version}.apk"
    else:
        file_name = f"摸鱼电视_{int(timestamp or time.time())}.apk"

    print(f"✅ 成功获取文件：{asset['name']} -> 重命名为 {file_name}（{size/1024/1024:.2f}MB）")

    proxy_urls = [url] + utils.get_proxy_urls(url, ["https://github.com"])
    file_info = {
        "name": file_name,
        "url": url,
        "size": size,
        "timestamp": timestamp,
        "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
    }

    stats = perform_downloads(
        task_name,
        [file_info],
        MOYU_DOWNLOAD_DIR,
        delete_old=True,
        file_pattern=r"摸鱼电视.*\.apk"
    )
    if stats["downloaded_files"]:
        stats["downloaded_file"] = stats["downloaded_files"][0]
    task_stats.update("moyu", task_name, stats)

@handle_errors
def run_xiaosa():
    task_name = "潇洒影包下载"
    print_section(task_name)
    owner = XIAOSA_GITHUB_REPO_OWNER
    repo = XIAOSA_GITHUB_REPO_NAME
    file_path = XIAOSA_FILE_PATH
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    file_name = os.path.basename(file_path)
    save_path = os.path.join(XIAOSA_DOWNLOAD_DIR, file_name)
    remote_size = None
    remote_timestamp = None
    raw_url = f"https://raw.githubusercontent.com/{owner}/{repo}/main/{file_path}"
    proxy_urls = utils.get_proxy_urls(raw_url, ["https://raw.githubusercontent.com"])

    for url in proxy_urls:
        try:
            resp = requests.head(url, timeout=10, allow_redirects=True, verify=False)
            if resp.status_code == 200:
                remote_size = int(resp.headers.get('Content-Length', 0))
                last_modified = resp.headers.get('Last-Modified')
                if last_modified:
                    remote_timestamp = datetime.strptime(last_modified, "%a, %d %b %Y %H:%M:%S %Z").timestamp()
                    break
        except:
            continue

    if remote_timestamp is None:
        encoded_name = urllib.parse.quote(file_path, safe='')
        for branch in ['main', 'master']:
            api_url = f"https://api.github.com/repos/{owner}/{repo}/commits?path={encoded_name}&sha={branch}&per_page=1"
            headers_api = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
            try:
                api_resp = session.get(api_url, headers=headers_api, timeout=10)
                if api_resp.status_code == 200:
                    commits = api_resp.json()
                    if commits:
                        commit_date = commits[0].get('commit', {}).get('committer', {}).get('date')
                        if commit_date:
                            remote_timestamp = datetime.strptime(commit_date, "%Y-%m-%dT%H:%M:%SZ").timestamp()
                            break
            except:
                continue

    if remote_size is None or remote_size == 0:
        print(f"⚠️ 远程文件不存在或大小为 0，跳过下载：{raw_url}")
        stats = {"total_files": 1, "success_count": 0, "skip_count": 0, "fail_count": 1,
                 "deleted_count": 0, "save_path": XIAOSA_DOWNLOAD_DIR, "downloaded_files": []}
        task_stats.print_summary(stats)
        task_stats.update("xiaosa", task_name, stats)
        return

    date_prefix = datetime.fromtimestamp(remote_timestamp).strftime(DATE_FORMAT_FILE) + "_"
    name_without_ext, ext = os.path.splitext(file_name)
    new_name = f"{date_prefix}{name_without_ext}{ext}"
    save_path = os.path.join(XIAOSA_DOWNLOAD_DIR, new_name)

    file_size_mb = remote_size / 1024 / 1024 if remote_size else 0.0

    print(f"✅ 成功获取文件：{file_name} -> 重命名为 {new_name}（{file_size_mb:.2f}MB）")

    if utils.is_file_current(save_path, remote_timestamp):
        print(f"⏩ 文件已最新，跳过下载：{new_name}")
        deleted_count = utils.clean_old_files(
            XIAOSA_DOWNLOAD_DIR,
            keep_filenames=[new_name],
            pattern=r".*单线路.*\.zip$",
            safe_mode=True
        )
        stats = {
            "total_files": 1,
            "success_count": 0,
            "skip_count": 1,
            "fail_count": 0,
            "deleted_count": deleted_count,
            "save_path": XIAOSA_DOWNLOAD_DIR,
            "downloaded_files": []
        }
        task_stats.print_summary(stats)
        task_stats.update("xiaosa", task_name, stats)
        return

    success, file_info = download_file_with_retry(
        url=raw_url,
        save_path=save_path,
        expected_size=remote_size,
        expected_timestamp=remote_timestamp,
        proxy_urls=proxy_urls,
        retries=2,
        timeout=120,
        verify=False
    )

    deleted_count = 0
    if success and file_info:
        deleted_count = utils.clean_old_files(
            XIAOSA_DOWNLOAD_DIR,
            keep_filenames=[new_name],
            pattern=r".*单线路.*\.zip$",
            safe_mode=True
        )
        extract_count = tg_extract_zip(save_path, XIAOSA_EXTRACT_DIR, is_chinese=True)
        stats = {
            "total_files": 1,
            "success_count": 1,
            "skip_count": 0,
            "fail_count": 0,
            "deleted_count": deleted_count,
            "save_path": XIAOSA_DOWNLOAD_DIR,
            "downloaded_files": [file_info],
            "extracted_count": extract_count,
            "extract_folder": XIAOSA_EXTRACT_DIR
        }
    elif success and file_info is None:
        stats = {
            "total_files": 1,
            "success_count": 0,
            "skip_count": 1,
            "fail_count": 0,
            "deleted_count": 0,
            "save_path": XIAOSA_DOWNLOAD_DIR,
            "downloaded_files": []
        }
    else:
        stats = {
            "total_files": 1,
            "success_count": 0,
            "skip_count": 0,
            "fail_count": 1,
            "deleted_count": 0,
            "save_path": XIAOSA_DOWNLOAD_DIR,
            "downloaded_files": []
        }

    task_stats.print_summary(stats)
    task_stats.update("xiaosa", task_name, stats)

def ok_list_files_batch(remote_dirs):
    all_files = []
    auth = (OK_USERNAME, OK_PASSWORD)

    def walk_directory(path):
        url = f"{OK_WEBDAV_BASE_URL}/{path}"
        headers = {"Depth": "1", "Content-Type": "application/xml"}
        data = """<d:propfind xmlns:d="DAV:"><d:prop><d:displayname/><d:getcontentlength/><d:getlastmodified/><d:resourcetype/></d:prop></d:propfind>"""
        try:
            r = requests.request("PROPFIND", url, headers=headers, data=data, auth=auth, timeout=30)
            if r.status_code != 207:
                return []
            root = ET.fromstring(r.text)
            items = []
            for item in root.findall(".//{DAV:}response"):
                href = item.find(".//{DAV:}href").text
                if href.rstrip('/').endswith(path.rstrip('/')):
                    continue
                name = os.path.basename(unquote(href.rstrip('/')))
                rt = item.find(".//{DAV:}resourcetype")
                is_dir = rt.find("{DAV:}collection") is not None if rt else False
                full = '/'.join([path, name]).strip('/')
                if is_dir:
                    items.extend(walk_directory(full))
                else:
                    lm = item.find(".//{DAV:}getlastmodified").text
                    try:
                        ts = datetime.strptime(lm, "%a, %d %b %Y %H:%M:%S GMT").timestamp()
                    except:
                        ts = time.time()
                    sz = int(item.find(".//{DAV:}getcontentlength").text or 0)
                    items.append({"name": name, "path": full, "size": sz, "timestamp": ts})
            return items
        except:
            return []

    for d in remote_dirs:
        files = walk_directory(d)
        apk_files = [f for f in files if f["name"].lower().endswith(".apk")]
        if d == "欧克标准":
            apk_files = [f for f in apk_files if not any(x in f["name"] for x in ["海信", "arm", "x5"])]
        apk_files.sort(key=lambda x: x["timestamp"], reverse=True)
        selected = apk_files[:OK_MAX_FILES_TO_SYNC]
        for f in selected:
            all_files.append({
                "name": f["name"],
                "url": f"{OK_WEBDAV_BASE_URL}/{f['path']}",
                "size": f["size"],
                "timestamp": f["timestamp"]
            })
    print(f"✅ 成功获取{len(all_files)}个文件的下载信息")
    return all_files

@handle_errors
def run_ok():
    task_name = "欧克影视下载"
    print_section(task_name)
    file_infos = ok_list_files_batch(OK_REMOTE_DIRS)
    if not file_infos:
        return
    stats = perform_downloads(
        task_name,
        file_infos,
        OK_MERGED_DIR,
        delete_old=True,
        file_pattern=r".*\.apk$",
        auth=(OK_USERNAME, OK_PASSWORD),
        retries=OK_MAX_RETRIES
    )
    task_stats.update("ok", task_name, stats)

@handle_errors
def run_tangshi():
    task_name = "唐三影视下载"
    print_section(task_name)
    api_url = f"https://api.github.com/repos/{TANGSHI_GITHUB_REPO_OWNER}/{TANGSHI_GITHUB_REPO_NAME}/contents/{TANGSHI_APK_PATH}?ref={TANGSHI_GITHUB_BRANCH}"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    response = session.get(api_url, headers=headers, timeout=30)
    response.raise_for_status()
    contents = response.json()
    if not isinstance(contents, list):
        return
    file_infos = []
    version = None
    try:
        commits_url = f"https://api.github.com/repos/{TANGSHI_GITHUB_REPO_OWNER}/{TANGSHI_GITHUB_REPO_NAME}/commits?sha={TANGSHI_GITHUB_BRANCH}&per_page=1"
        commits_resp = session.get(commits_url, headers=headers, timeout=10)
        if commits_resp.status_code == 200:
            commits = commits_resp.json()
            if commits and commits[0].get('commit', {}).get('message'):
                commit_msg = commits[0]['commit']['message']
                version_match = re.search(r'Update to (\d+(?:\.\d+)+)', commit_msg)
                if version_match:
                    version = version_match.group(1)
    except:
        pass
    for item in contents:
        if item.get("type") == "file" and item["name"].lower().endswith(".apk"):
            timestamp = None
            try:
                commits_url = f"https://api.github.com/repos/{TANGSHI_GITHUB_REPO_OWNER}/{TANGSHI_GITHUB_REPO_NAME}/commits?path={TANGSHI_APK_PATH}/{item['name']}&sha={TANGSHI_GITHUB_BRANCH}&per_page=1"
                commits_resp = session.get(commits_url, headers=headers, timeout=10)
                if commits_resp.status_code == 200:
                    commits = commits_resp.json()
                    if commits and commits[0].get('commit', {}).get('committer', {}).get('date'):
                        commit_date = commits[0]['commit']['committer']['date']
                        timestamp = datetime.strptime(commit_date, "%Y-%m-%dT%H:%M:%SZ").timestamp()
            except:
                pass
            if timestamp is not None:
                if version:
                    version_str = version
                else:
                    version_str = datetime.fromtimestamp(timestamp).strftime(DATE_FORMAT_FILE)
                name_without_ext, ext = os.path.splitext(item['name'])
                new_name = f"{name_without_ext}_v{version_str}{ext}"
                raw_url = item.get("download_url")
                proxy_urls = [raw_url] + utils.get_proxy_urls(raw_url, ["https://raw.githubusercontent.com"])
                file_infos.append({
                    "name": new_name,
                    "url": raw_url,
                    "size": item.get("size", 0),
                    "timestamp": timestamp,
                    "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
                })
    print(f"✅ 成功获取{len(file_infos)}个文件的下载信息")
    if not file_infos:
        return
    stats = perform_downloads(task_name, file_infos, TANGSHI_DOWNLOAD_DIR, delete_old=True, file_pattern=r".*\.apk$")
    task_stats.update("tangshi", task_name, stats)

@handle_errors
def run_fish():
    task_name = "鱼佬影视下载"
    print_section(task_name)
    api_url = f"https://api.github.com/repos/{FISH_GITHUB_REPO_OWNER}/{FISH_GITHUB_REPO_NAME}/releases"
    headers = {"Authorization": f"token {MYDV_GITHUB_TOKEN}"} if MYDV_GITHUB_TOKEN else {}
    session = requests.Session()
    session.mount('https://', CustomHTTPAdapter())
    session.verify = False
    max_retries = 3
    response = None
    for attempt in range(max_retries):
        try:
            response = session.get(api_url, headers=headers, timeout=60)
            response.raise_for_status()
            break
        except:
            if attempt == max_retries - 1:
                raise
            time.sleep(3)
    releases = response.json()
    if not releases:
        return
    releases.sort(key=lambda x: x.get('published_at', ''), reverse=True)
    latest_release = releases[0]
    tag_name = latest_release.get("tag_name", "")
    published_at = latest_release.get("published_at")
    timestamp = datetime.strptime(published_at, "%Y-%m-%dT%H:%M:%SZ").timestamp() if published_at else None
    full_version = tag_name
    if full_version.startswith(('v', 'V')):
        full_version = full_version[1:]
    file_infos = []
    for asset in latest_release.get("assets", []):
        if asset["name"].lower().endswith(".apk"):
            url = asset["browser_download_url"]
            size = asset["size"]
            original_name = asset["name"]
            asset_updated = asset.get("updated_at") or asset.get("created_at")
            if asset_updated:
                asset_timestamp = datetime.strptime(asset_updated, "%Y-%m-%dT%H:%M:%SZ").timestamp()
            else:
                asset_timestamp = timestamp
            name_without_ext, ext = os.path.splitext(original_name)
            file_name = f"{name_without_ext}_{full_version}{ext}"
            proxy_urls = [url] + utils.get_proxy_urls(url, ["https://github.com"])
            file_infos.append({
                "name": file_name,
                "url": url,
                "size": size,
                "timestamp": asset_timestamp,
                "extra_download_kwargs": {"proxy_urls": proxy_urls, "retries": 2, "timeout": 120}
            })
    print(f"✅ 成功获取 {len(file_infos)} 个文件的下载信息")
    if not file_infos:
        return
    stats = perform_downloads(
        task_name,
        file_infos,
        FISH_DOWNLOAD_DIR,
        delete_old=True,
        file_pattern=r".*\.apk$"
    )
    task_stats.update("fish", task_name, stats)

class TG_Config:
    def __init__(self, name, channel, dl, ex, fl, st='telegram', target_name=None, clean_pattern=None):
        self.name = name
        self.channel = channel
        self.download_path = dl
        self.extract_folder = ex
        self.file_filter = fl
        self.source_type = st
        self.target_name = target_name
        self.clean_pattern = clean_pattern

tg_configs = [
    TG_Config('油管', 'https://t.me/SmartTubeNewsEN', '/mnt/sda1/webdav/电视油管/', '/mnt/sda1/webdav/电视油管', r".*SmartTube_stable.*\.apk"),
    TG_Config('快手', 'https://t.me/mytv_android_channel', '/mnt/sda1/webdav/电视抖音/', '/mnt/sda1/webdav/电视抖音/', r"myks\d+\.\d+\.apk$", clean_pattern=r"迷你快手.*\.apk"),
    TG_Config('熊猫', 'https://t.me/PandaGroovePG', '/mnt/sda1/webdav/本地影包/', '/mnt/sda1/html/pg/pgdown', r"pg\.\d{8}-\d{4}\.zip"),
    TG_Config('真心', 'https://t.me/juejijianghu', '/mnt/sda1/webdav/本地影包/', '/mnt/sda1/html/pg/zhenxin', r"真心\d{8}(?:-\d+)?-?(?:增量包|完整包)\.zip"),
    TG_Config('列表', 'https://t.me/mytv_android_channel', '/var/www/html/pg/js/', '/var/www/html/pg/js/', r"国内香港英超\.txt", 'telegram', target_name='国内香港.txt'),
]

tg_pbar = None

def tg_download_progress(current, total):
    global tg_pbar
    if total == 0:
        return
    if tg_pbar is None or tg_pbar.total != total:
        tg_pbar = tqdm(total=total, unit='B', unit_scale=True, desc="下载 Telegram文件", leave=False)
    tg_pbar.update(current - tg_pbar.n)
    if current == total:
        tg_pbar.close()
        tg_pbar = None

async def tg_process_package(client, cfg):
    res = {
        "name": cfg.name,
        "status": "skipped",
        "has_update": False,
        "msg": "",
        "file_name": "",
        "file_size": 0,
        "extracted_count": 0,
        "extract_folder": cfg.extract_folder,
        "deleted_count": 0
    }
    temp_path = None
    try:
        if cfg.source_type == 'telegram':
            msgs = await client.get_messages(cfg.channel, limit=1000)
            matches = []
            for m in msgs:
                if m.document:
                    for a in m.document.attributes:
                        if isinstance(a, DocumentAttributeFilename) and re.match(cfg.file_filter, a.file_name):
                            matches.append((m, a.file_name, m.document.size))
            if not matches:
                res["msg"] = "未找到文件"
                return res
            matches.sort(key=lambda x: x[0].date, reverse=True)
            m, fn, sz = matches[0]
            timestamp = m.date.timestamp()
            if cfg.name == "快手":
                version_match = re.search(r'(\d+(?:\.\d+)+)', fn)
                if version_match:
                    version = version_match.group(1)
                    final_name = f"迷你快手{version}.apk"
                else:
                    final_name = f"迷你快手_{int(timestamp)}.apk"
            else:
                final_name = cfg.target_name if cfg.target_name else fn
            res["file_name"] = final_name
            res["file_size"] = sz / 1024 / 1024
            save_path = os.path.join(cfg.download_path, final_name)
            if utils.is_file_current(save_path, timestamp):
                res["msg"] = f"文件已最新，跳过下载：{final_name}"
                return res
            temp_path = save_path + ".tmp"
            await m.download_media(temp_path, progress_callback=tg_download_progress)
            if os.path.getsize(temp_path) != sz:
                utils.safe_remove_temp(temp_path)
                raise ValueError(f"文件大小不匹配")
            file_info = finalize_download(temp_path, save_path, timestamp)
            temp_path = None
            if fn.lower().endswith('.zip'):
                extract_count = tg_extract_zip(save_path, cfg.extract_folder, cfg.name == '真心')
                res["extracted_count"] = extract_count
            else:
                if cfg.name != "油管":
                    utils.ensure_dir(cfg.extract_folder)
                    copied_path = os.path.join(cfg.extract_folder, final_name)
                    if os.path.abspath(save_path) != os.path.abspath(copied_path):
                        shutil.copy2(save_path, copied_path)
                        os.utime(copied_path, (timestamp, timestamp))
                    res["extracted_count"] = 1
            res["status"] = "success"
            res["has_update"] = True
            res["msg"] = "下载完成"
            clean_pattern = cfg.clean_pattern if cfg.clean_pattern else cfg.file_filter
            res["deleted_count"] = utils.clean_old_files(cfg.download_path, keep_filenames=[final_name], pattern=clean_pattern, safe_mode=True)
    except Exception as e:
        res["status"] = "error"
        res["msg"] = "异常"
        if temp_path and os.path.exists(temp_path):
            utils.safe_remove_temp(temp_path)
    return res

async def tg_main(client):
    config_names = "、".join([cfg.name for cfg in tg_configs])
    print(f"✅ 成功获取{config_names}的下载信息")
    package_details = []
    success_count = 0
    for cfg in tg_configs:
        r = await tg_process_package(client, cfg)
        package_details.append(r)
        if r["has_update"]:
            success_count += 1
        icon = "⏩" if r["status"] == "skipped" else "✅" if r["status"] == "success" else "❌"
        delete_info = f" 🧹 删除{r['deleted_count']}" if r['deleted_count'] > 0 else ""
        display_name = "列表" if cfg.name == "国内香港" else cfg.name
        print(f"{icon} {display_name}：{r['msg']}{delete_info}")
    return package_details, success_count

@handle_errors
def run_tg():
    task_name = "飞机电报下载"
    print_section(task_name)
    client = TelegramClient(TG_SESSION_FILE, TG_API_ID, TG_API_HASH)
    client.start()
    with client:
        package_details, success_count = client.loop.run_until_complete(tg_main(client))
    total_packages = len(tg_configs)
    total_deleted = sum(pkg.get('deleted_count', 0) for pkg in package_details)
    print(f"📊 执行完成：总文件数{total_packages} ✅ 成功{success_count} ⏩ 跳过{total_packages - success_count} 🧹 删除{total_deleted}")
    task_stats.update("tg", task_name, {"success_count": success_count, "package_details": package_details})

def send_combined_notification():
    if not task_stats.has_update():
        print_line()
        print("⚠️ 所有任务均未更新，不发送通知")
        print_line()
        return
    content = "【软件同步报告】\n\n"
    main_tasks = ["ku9", "yygj", "mydv", "mydv_lite", "blbl", "moyu", "xiaosa", "ok", "tangshi", "fish"]
    task_display = {
        "ku9": "酷九", "yygj": "管家", "mydv": "电视抖音", "mydv_lite": "迷你抖音",
        "blbl": "哔哩", "moyu": "摸鱼", "xiaosa": "潇洒", "ok": "欧克", "tangshi": "唐三", "fish": "鱼佬"
    }
    for key in main_tasks:
        task = task_stats.get_task(key)
        if not task["has_update"]:
            continue
        name = task_display.get(key, key)
        stats = task["data"]
        content += f"［{name}］\n"
        if "downloaded_files" in stats:
            files = stats["downloaded_files"]
            if files:
                content += f"  新增：{len(files)} 个文件\n"
                content += "  列表：\n"
                for f in files[:5]:
                    sz = f.get("size_mb", f.get("size", 0)) / 1024 / 1024
                    content += f"    - {f['name']}（{sz:.2f}MB）\n"
                if len(files) > 5:
                    content += f"    - ... 共 {len(files)} 个文件\n"
        elif "downloaded_file" in stats and stats["downloaded_file"]:
            f = stats["downloaded_file"]
            sz = f.get("size", 0) / 1024 / 1024
            content += f"  新增：1 个文件\n"
            content += f"    - {f['name']}（{sz:.2f}MB）\n"
        if stats.get("deleted_count", 0) > 0:
            content += f"  删除：{stats['deleted_count']} 个旧文件\n"
        if stats.get("extracted_count", 0) > 0:
            extract_path = stats.get("extract_folder", "")
            if extract_path:
                content += f"  解压：{stats['extracted_count']} 个文件到 {extract_path}\n"
            else:
                content += f"  解压：{stats['extracted_count']} 个文件\n"
        content += "\n"
    tg_task = task_stats.get_task("tg")
    if tg_task["has_update"]:
        packages = tg_task["data"].get("package_details", [])
        for pkg in packages:
            if pkg["has_update"]:
                content += f"［{pkg['name']}］\n"
                content += f"  新增：1 个文件\n"
                content += f"    - 名称：{pkg['file_name']}\n"
                if pkg.get('file_size', 0) > 0:
                    content += f"    - 大小：{pkg['file_size']:.2f}MB\n"
                if pkg['name'] != '油管' and pkg.get('extracted_count', 0) > 0:
                    content += f"    - 解压：{pkg['extract_folder']}\n"
                if pkg.get('deleted_count', 0) > 0:
                    content += f"    - 删除：{pkg['deleted_count']} 个旧文件\n"
                content += "\n"
    payload = {"msgtype": "text", "text": {"content": content.rstrip("\n")}}
    try:
        response = requests.post(WEBHOOK_URL, headers={"Content-Type": "application/json"}, data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), timeout=TIMEOUT)
        response.raise_for_status()
        if response.json().get("errcode") == 0:
            print("✅ 合并通知发送成功！")
    except:
        pass

def main():
    print_line()
    print("🎯 启动软件批量下载小工具")
    run_ku9()
    run_yygj()
    run_mydv()
    run_mydv_lite()
    run_blbl()
    run_moyu()
    run_xiaosa()
    run_ok()
    run_tangshi()
    run_fish()
    run_tg()
    send_combined_notification()
    print("🎉 所有任务执行完毕！")
    print_line()

if __name__ == "__main__":
    main()