公众号旧文

我的 YouTube 下载站,第一次被自然访问了

作者:胖子阅读 —

最近一直在 Anti Content Theft,都没怎么鼓捣服务器了。

https://github.com/pangzibaike/anti-content-theft

刚打开流量统计才发现,这几天下载机的流量有了异常增长。

图片

比如 5 月 6 日、5 月 8 日,都出现了与平日极为不同的流量消耗。

于是我开始在所有 nginx 日志里筛选。

最后,还真让我筛出了几个真实用户 IP。

图片

第一名下载了接近 500MB 的视频。

图片

请求非常规律。

先提交任务:

POST /task

随后不断轮询:

GET /status/xxx

最后开始下载:

GET /videos/xxx.mp4

跑完了整个下载流程。

还能从 User-Agent 里看出来,对方用的是 HarmonyOS 手机。

Mozilla/5.0 (Linux; Android 12; HarmonyOS; ADY-AL00)

也就是说,真的有陌生人,用手机访问了我做的下载站。

后端日志里,还记录了用户提交的 YouTube 视频链接。

因为 yt-dlp 下载时,会输出 Extracting URL

于是我直接开始翻 journalctl -u ytdlp

就把用户提交过的视频链接全统计出来了。

图片

第一名用的是手机,上图中的短链,显然就是他下载的那些视频。

第二名用的电脑,UA如下:

Mozilla/5.0 (Windows NT 10.0; Win64; x64)
Chrome/147

他俩一个看的日本男团,一个看的英国纪录片。

图片

我建站的目的,一方面是机器闲着也是闲着,一方面是想看看大家都对什么内容感兴趣,用大家感兴趣的视频,来拓宽一下我自己的视野。

最后分享一下项目结构:

/opt/ytdlp
├── server.py        后端 Flask 服务
├── index.html       前端页面
├── yt-dlp           下载核心程序
├── cookies.txt      YouTube cookies
├── videos/          临时视频文件目录
├── venv/            Python 虚拟环境
└── gunicorn.ctl     gunicorn 启动配置

再分享一下代码:

/opt/ytdlp/server.py

from flask import Flask, request, jsonify, send_file, send_from_directory
import os
import uuid
import subprocess
import threading
import time
import shutil
import re

app = Flask(__name__)

VIDEO_DIR = "videos"
YTDLP = "/opt/ytdlp/yt-dlp"

FILE_EXPIRE = 3600  # 1小时删除
MIN_FREE_SPACE = 1024 * 1024 * 1024  # 最少需要1GB空闲

TASKS = {}
DOWNLOADING = False

if not os.path.exists(VIDEO_DIR):
    os.makedirs(VIDEO_DIR)


@app.route("/")
def home():
    return send_file("index.html")


# 检查磁盘空间
def check_disk_space():

    total, used, free = shutil.disk_usage("/")

    if free < MIN_FREE_SPACE:
        return False

    return True


# 创建任务
@app.route("/task", methods=["POST"])
def create_task():

    cleanup_once()

    global DOWNLOADING

    data = request.json
    url = data.get("url")

    if not url:
        return jsonify({"error": "no url"})

    if "youtube.com" not in url and "youtu.be" not in url:
        return jsonify({"error": "invalid url"})

    if DOWNLOADING:
        return jsonify({
            "error": "busy",
            "message": "当前已经有下载任务正在进行"
        })

    if not check_disk_space():
        return jsonify({
            "error": "disk_full",
            "message": "服务器磁盘空间不足"
        })

    task_id = str(uuid.uuid4())

    TASKS[task_id] = {
        "status": "downloading",
        "file": None,
        "time": time.time()
    }

    DOWNLOADING = True

    thread = threading.Thread(target=download_video, args=(task_id, url))
    thread.daemon = True
    thread.start()

    return jsonify({"task": task_id})


# 查询任务
@app.route("/status/<task_id>")
def task_status(task_id):

    task = TASKS.get(task_id)

    if not task:
        return jsonify({"error": "task not found"})

    return jsonify(task)


# 下载视频
def download_video(task_id, url):

    global DOWNLOADING

    try:

        output = f"{VIDEO_DIR}/{task_id}.%(ext)s"

        cmd = [
            YTDLP,
            "--js-runtimes", "node",
            "--no-playlist",
            "--concurrent-fragments", "3",
            "--throttled-rate", "100K",
            "-f bv*+ba/b",
            "--merge-output-format", "mp4",
            "-o", output,
            url
        ]

        subprocess.run(cmd, check=True)

        TASKS[task_id]["status"] = "finished"
        for file in os.listdir(VIDEO_DIR):
            if file.startswith(task_id):
                TASKS[task_id]["file"] = file

    except Exception as e:

        TASKS[task_id]["status"] = "error"
        TASKS[task_id]["error"] = str(e)

    finally:

        DOWNLOADING = False


# 下载文件
@app.route("/videos/<path:filename>")
def videos(filename):
    return send_from_directory(VIDEO_DIR, filename)


if __name__ == "__main__":

    app.run(host="0.0.0.0", port=5001)

def cleanup_once():

    now = time.time()

    for file in os.listdir(VIDEO_DIR):

        path = os.path.join(VIDEO_DIR, file)

        if os.path.isfile(path):

            if now - os.path.getmtime(path) > FILE_EXPIRE:
                os.remove(path)

@app.route("/recent")
def recent_downloads():

    try:

        result = subprocess.check_output(
            "journalctl -u ytdlp -n 200 | grep 'Extracting URL'",
            shell=True
        ).decode()

        videos = []

        for line in result.splitlines():

            m = re.search(r'(https://www\.youtube\.com/watch\?v=[\w-]+|https://youtu\.be/[\w-]+)', line)

            if m:
                videos.append(m.group(0))

        # 去重并保留顺序
        seen = set()
        unique = []

        for v in videos[::-1]:
            if v not in seen:
                unique.append(v)
                seen.add(v)

        return jsonify(unique[:10])

    except Exception:
        return jsonify([])

扩展阅读:

关于“胖子百科”被“网络技术联盟站”多次洗稿的说明

关于“胖子百科”被瑞哥小号“网络技术干货圈”更多次洗稿的说明

给 网络技术联盟站 瑞哥 的一封公开信