最近使用tg2drive自动追剧到云盘,但是对于码率比较高的电视剧在观看时会很卡,几乎一秒一卡,于是有了本脚本的出现。

通过监控tg2drive生成的strm文件夹,通过关键词匹配下载指定电视剧,实现观看时不再卡顿,自动跳过已下载文件,由于监控的是生成到本地的strm文件,因此全程无云盘风控风险,扫描间隔在青龙面板配置想多短就多短。

对于其他strm方案只要strm内容是http直链均可正常使用,如果需要的人多也可以改一版webdav挂载方案。


至于为什么使用青龙面板而不是发一个docker,一来功能简单,二来实在是内存不够了

#!/usr/bin/env python3
"""
青龙面板定时脚本:扫描 strm 文件夹,下载关键词匹配的文件,保持目录结构
"""

import os
import re
import time
import logging
import shutil
from urllib.parse import unquote, urlparse
from pathlib import Path

import requests

# ---------- 配置(根据实际情况修改)----------
STRM_DIR = "/volume1/docker/tg2drive/strm/123云盘/library/剧集"
DOWNLOAD_DIR = "/volume5/workspace/云盘下载"
KEYWORDS = ["小芳"]          # 文件名包含任意一个关键词即下载
RECURSIVE = True             # 是否递归扫描子目录
DOWNLOAD_TIMEOUT = 300       # 下载超时(秒)
RETRY_TIMES = 3              # 重试次数
# --------------------------------------------

logging.basicConfig(
    level=logging.INFO,
    format='%(asctime)s - %(levelname)s - %(message)s'
)
logger = logging.getLogger(__name__)

def ensure_dir(path):
    """确保目录存在"""
    os.makedirs(path, exist_ok=True)

def extract_filename_from_url(url):
    """从 URL 中提取文件名"""
    parsed = urlparse(url)
    path = unquote(parsed.path)
    filename = os.path.basename(path)
    if not filename:
        # 尝试从查询参数中提取(某些链接带 filename 参数)
        query = parsed.query
        match = re.search(r'filename=([^&]+)', query)
        if match:
            filename = unquote(match.group(1))
    return filename

def should_download(filename):
    """根据关键词判断是否应该下载"""
    if not filename:
        return False
    for kw in KEYWORDS:
        if kw in filename:
            return True
    return False

def download_file(url, dest_path):
    """下载文件到指定路径,带重试(每10%打印一次进度)"""
    for attempt in range(1, RETRY_TIMES + 1):
        try:
            logger.info(f"开始下载 [{attempt}/{RETRY_TIMES}]: {dest_path.name}")
            with requests.get(url, stream=True, timeout=DOWNLOAD_TIMEOUT) as r:
                r.raise_for_status()
                total_size = int(r.headers.get('content-length', 0))
                temp_path = str(dest_path) + ".tmp"
                with open(temp_path, 'wb') as f:
                    downloaded = 0
                    last_percent = -1
                    for chunk in r.iter_content(chunk_size=8192):
                        if chunk:
                            f.write(chunk)
                            downloaded += len(chunk)
                            if total_size:
                                percent = int((downloaded / total_size) * 100)
                                if percent >= last_percent + 10:
                                    logger.info(f"下载进度: {dest_path.name} {percent}%")
                                    last_percent = percent
                shutil.move(temp_path, str(dest_path))
                logger.info(f"下载完成: {dest_path.name} (大小: {total_size} bytes)")
                return True
        except Exception as e:
            logger.error(f"下载失败 (尝试 {attempt}): {e}")
            temp_path = str(dest_path) + ".tmp"
            if os.path.exists(temp_path):
                os.remove(temp_path)
            if attempt == RETRY_TIMES:
                logger.error(f"下载最终失败: {dest_path.name}")
                return False
            time.sleep(2 ** attempt)
    return False
    
def process_strm_file(strm_path):
    """
    处理单个 strm 文件
    返回:
        1  = 下载成功
        0  = 文件已存在,跳过
        -1 = 下载失败
        -2 = 关键词不匹配,跳过
    """
    # 读取 URL
    try:
        with open(strm_path, 'r', encoding='utf-8') as f:
            url = f.readline().strip()
    except Exception as e:
        logger.error(f"读取 strm 文件失败 {strm_path}: {e}")
        return -1

    if not url:
        logger.warning(f"strm 文件内容为空: {strm_path}")
        return -1

    # 提取文件名
    filename = extract_filename_from_url(url)
    if not filename:
        filename = Path(strm_path).stem
        logger.warning(f"无法从 URL 提取文件名,使用 strm 文件名: {filename}")

    # 关键词过滤
    if not should_download(filename):
        return -2  # 关键词不匹配

    # 计算目标路径
    rel_path = os.path.relpath(strm_path, STRM_DIR)
    rel_dir = os.path.dirname(rel_path)
    target_dir = Path(DOWNLOAD_DIR) / rel_dir
    dest_path = target_dir / filename

    # 检查是否已存在
    if dest_path.exists():
        logger.debug(f"文件已存在,跳过: {dest_path}")  # debug 级别,不显示
        return 0

    # 确保目录存在
    ensure_dir(target_dir)

    # 下载
    success = download_file(url, dest_path)
    if success:
        logger.info(f"成功下载: {filename} 到 {dest_path}")
        return 1
    else:
        logger.error(f"下载失败: {filename}")
        return -1

def main():
    logger.info("===== 开始扫描 strm 文件 =====")
    ensure_dir(DOWNLOAD_DIR)

    strm_files = []
    if RECURSIVE:
        for root, _, files in os.walk(STRM_DIR):
            for f in files:
                if f.lower().endswith('.strm'):
                    strm_files.append(os.path.join(root, f))
    else:
        for f in os.listdir(STRM_DIR):
            if f.lower().endswith('.strm'):
                strm_files.append(os.path.join(STRM_DIR, f))

    total = len(strm_files)
    logger.info(f"共发现 {total} 个 strm 文件")

    # 统计
    skipped_exist = 0
    skipped_keyword = 0
    downloaded = 0
    failed = 0

    for strm_path in strm_files:
        status = process_strm_file(strm_path)
        if status == 1:
            downloaded += 1
        elif status == 0:
            skipped_exist += 1
        elif status == -2:
            skipped_keyword += 1
        else:  # -1
            failed += 1

    # 最终统计信息
    logger.info("===== 扫描完成 =====")
    logger.info(f"总计扫描 strm 文件: {total}")
    logger.info(f"已存在跳过: {skipped_exist}")
    logger.info(f"关键词不匹配跳过: {skipped_keyword}")
    logger.info(f"下载成功: {downloaded}")
    logger.info(f"下载失败: {failed}")

if __name__ == "__main__":
    main()