提示

返回博客列表

不用买服务器,用 GitHub Actions 自动下载和监控视频更新

不用买服务器,用 GitHub Actions 自动下载和监控视频更新

你关注了几个 YouTube 频道,想第一时间下载新视频。或者你需要定期备份某个平台的教程合集。买个服务器跑定时任务太浪费,用自己的电脑又不可能 24 小时开机。其实有一个完全免费、合法、稳定的方案——GitHub Actions。它每月提供 2000 分钟的免费运行时间(公开仓库无限),足够你每天跑几十次视频下载任务。这篇文章教你搭建一套完整的自动化管线:定时触发 → 视频下载 → 去重 → 通知推送(钉钉/微信/Telegram),全程不需要一台自己的服务器。

TL;DR:核心架构:GitHub Actions cron 定时触发 → 运行 Python 脚本(yt-dlp 下载)→ 文件存到 GitHub Releases 或上传到云存储 → 钉钉/微信机器人推送通知。关键技巧:用 --download-archive 避免重复下载,用 GitHub Secrets 存敏感信息(Cookie/Token),文件超过 100MB 分流到 Releases 或 OSS。

目录

一、GitHub Actions 免费额度够用吗

先算一笔账:

仓库类型 每月免费时长 单次任务耗时(下载+处理) 每天可运行次数
公开仓库 无限 5 分钟 无限制
私有仓库 2000 分钟 5 分钟 ~13 次/天

对于监控视频更新这个场景——你不需要每分钟都检查,每小时一次足够了。所以:

公开仓库:每小时一次 × 24小时 × 5分钟 = 120分钟/天,无限额度绰绰有余
私有仓库:每天 6-8 次检查,完全在 2000 分钟以内

强烈建议用公开仓库——不仅免费额度无限,而且你的配置和脚本对别人也是个参考。敏感信息(Cookie、Token、Webhook 地址)通过 GitHub Secrets 存储,不会泄露。

二、最小可用方案:10 分钟搭好

2.1 目录结构

video-monitor/
├── .github/
│   └── workflows/
│       └── download.yml     # Actions 工作流
├── scripts/
│   └── download.py          # 下载脚本
├── archive.txt              # 下载记录(避免重复)
├── requirements.txt         # Python 依赖
└── README.md

2.2 下载脚本

# scripts/download.py
"""GitHub Actions 视频下载脚本"""
import subprocess
import sys
import os
import json
from datetime import datetime

# 从环境变量读取配置
CHANNELS = os.getenv('CHANNELS', '{}')  # JSON 格式的频道列表
COOKIES = os.getenv('COOKIES', '')       # Netscape 格式的 Cookie
OUTPUT_DIR = os.getenv('OUTPUT_DIR', './downloads')

def load_channels():
    """加载频道配置"""
    # 格式:{"频道名": ["URL1", "URL2"]}
    try:
        return json.loads(CHANNELS)
    except json.JSONDecodeError:
        # 也支持从文件读取
        with open('channels.json', 'r', encoding='utf-8') as f:
            return json.load(f)


def download_channel(name, urls):
    """下载指定频道的所有视频"""
    os.makedirs(f'{OUTPUT_DIR}/{name}', exist_ok=True)

    # 写入临时 Cookie 文件
    cookie_file = None
    if COOKIES:
        cookie_file = '/tmp/cookies.txt'
        with open(cookie_file, 'w') as f:
            f.write(COOKIES)

    for url in urls:
        cmd = [
            'yt-dlp',
            '--download-archive', 'archive.txt',  # 记录已下载,避免重复
            '--no-overwrites',                     # 不覆盖已有文件
            '--write-info-json',                   # 保存元数据
            '--write-thumbnail',                   # 保存缩略图
            '--format', 'bestvideo[height<=1080]+bestaudio/best[height<=1080]',
            '--merge-output-format', 'mp4',
            '--output', f'{OUTPUT_DIR}/{name}/%(upload_date)s - %(title).100s.%(ext)s',
            '--sleep-interval', '2',               # 请求间隔(避免被限)
            '--max-sleep-interval', '5',
            '--retries', '3',
            url,
        ]

        if cookie_file:
            cmd.extend(['--cookies', cookie_file])

        try:
            result = subprocess.run(cmd, capture_output=True, text=True, timeout=600)
            if result.returncode == 0:
                print(f"[OK] {name}: {url}")
            else:
                # yt-dlp 返回非零不一定失败(可能是视频已存在)
                if 'has already been recorded in the archive' in result.stdout:
                    print(f"[SKIP] {name}: 已下载过")
                else:
                    print(f"[FAIL] {name}: {result.stderr[:200]}")
        except subprocess.TimeoutExpired:
            print(f"[TIMEOUT] {name}: 下载超时")

    # 清理
    if cookie_file and os.path.exists(cookie_file):
        os.remove(cookie_file)


def generate_report():
    """生成下载报告"""
    report = {
        'date': datetime.now().isoformat(),
        'files': []
    }

    for root, dirs, files in os.walk(OUTPUT_DIR):
        for f in files:
            if f.endswith('.mp4'):
                path = os.path.join(root, f)
                size = os.path.getsize(path)
                report['files'].append({
                    'name': f,
                    'size_mb': round(size / 1024 / 1024, 1),
                })

    report['total'] = len(report['files'])
    report['total_size_mb'] = round(
        sum(f['size_mb'] for f in report['files']), 1
    )

    with open(f'{OUTPUT_DIR}/report.json', 'w') as f:
        json.dump(report, f, ensure_ascii=False, indent=2)

    print(f"\n=== 下载报告 ===")
    print(f"文件数: {report['total']}")
    print(f"总大小: {report['total_size_mb']} MB")
    for f in report['files']:
        print(f"  {f['name']} ({f['size_mb']} MB)")

    return report


if __name__ == '__main__':
    channels = load_channels()
    print(f"监控 {len(channels)} 个频道")

    for name, urls in channels.items():
        print(f"\n--- {name} ---")
        download_channel(name, urls)

    generate_report()

2.3 channels.json 配置

{
  "Fireship": [
    "https://www.youtube.com/@Fireship/videos"
  ],
  "阮一峰": [
    "https://space.bilibili.com/123456/video"
  ],
  "我的收藏": [
    "https://www.youtube.com/playlist?list=PLxxx"
  ]
}

2.4 GitHub Actions 工作流

# .github/workflows/download.yml
name: 视频下载

on:
  schedule:
    # 每 6 小时运行一次(UTC 时间)
    - cron: '0 */6 * * *'
  workflow_dispatch:  # 允许手动触发

jobs:
  download:
    runs-on: ubuntu-latest
    timeout-minutes: 30

    steps:
      - name: 检出代码
        uses: actions/checkout@v4

      - name: 安装 Python
        uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: 安装 FFmpeg
        run: |
          sudo apt-get update
          sudo apt-get install -y ffmpeg

      - name: 安装依赖
        run: |
          pip install yt-dlp

      - name: 运行下载脚本
        env:
          CHANNELS: ${{ secrets.CHANNELS }}
          COOKIES: ${{ secrets.COOKIES }}
        run: python scripts/download.py

      - name: 提交下载记录
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add archive.txt downloads/report.json
          git diff --staged --quiet || (
            git commit -m "更新下载记录 [skip ci]" &&
            git push
          )

      - name: 上传文件到 Artifacts
        uses: actions/upload-artifact@v4
        with:
          name: downloaded-videos
          path: downloads/
          retention-days: 7  # 保留 7 天

2.5 设置 GitHub Secrets

在仓库 Settings → Secrets and variables → Actions → 添加:

Secret 名 内容 说明
CHANNELS channels.json 的内容 监控的频道列表
COOKIES cookies.txt 的内容 (可选)登录态 Cookie

三、进阶:增量下载与去重

3.1 --download-archive 的原理

yt-dlp 的 --download-archive 参数会在每次成功下载后,把视频 ID 写入一个文本文件:

# archive.txt(自动维护)
youtube dQw4w9WgXcQ
bilibili BV1xx411c7mD
youtube 9bZkp7q19f0

下次运行时,yt-dlp 扫描频道的视频列表,跳过 archive.txt 中已有的 ID。这样就不会重复下载。

注意archive.txt 需要在每次 Actions 运行后提交回仓库,否则下次运行时会丢失记录。上面的工作流已经包含了这一步。

3.2 检测"新视频"而非"所有视频"

有时候你不只是想下载,还想知道"有没有新视频"——即使不下载也要通知你。可以先用 yt-dlp 的 --dump-json 获取视频列表,对比 archive.txt:

import subprocess
import json

def check_new_videos(channel_url, archive_file='archive.txt'):
    """检查频道是否有新视频(不下载)"""
    # 获取视频列表(JSON 格式,只取最近 10 个)
    cmd = [
        'yt-dlp',
        '--dump-json',
        '--playlist-end', '10',
        '--flat-playlist',
        channel_url,
    ]

    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode != 0:
        return []

    # 加载已下载记录
    downloaded = set()
    if os.path.exists(archive_file):
        with open(archive_file) as f:
            downloaded = set(line.strip() for line in f)

    # 解析视频列表
    new_videos = []
    for line in result.stdout.strip().split('\n'):
        if not line:
            continue
        video = json.loads(line)
        vid = video.get('id', '')

        if vid and vid not in downloaded:
            new_videos.append({
                'id': vid,
                'title': video.get('title', ''),
                'url': video.get('webpage_url', video.get('url', '')),
                'duration': video.get('duration', 0),
            })

    return new_videos

四、处理大文件:突破 100MB 限制

GitHub 单个文件限制 100MB,Artifacts 单文件 2GB 但总存储有限。对于大视频,有几个分流方案:

4.1 上传到 GitHub Releases

- name: 上传到 Releases
  uses: softprops/action-gh-release@v2
  with:
    tag_name: downloads-${{ github.run_id }}
    files: downloads/*.mp4
  env:
    GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

4.2 上传到阿里云 OSS / AWS S3

# 在下载脚本中添加
import oss2  # pip install oss2

def upload_to_oss(filepath, oss_config):
    """上传到阿里云 OSS"""
    auth = oss2.Auth(oss_config['access_key'], oss_config['access_secret'])
    bucket = oss2.Bucket(auth, oss_config['endpoint'], oss_config['bucket'])

    remote_name = os.path.basename(filepath)
    bucket.put_object_from_file(remote_name, filepath)
    print(f"已上传到 OSS: {remote_name}")

    # 生成临时下载链接
    url = bucket.sign_url('GET', remote_name, 86400)  # 24 小时有效
    return url

4.3 上传到 Cloudflare R2(免费 10GB)

import boto3

def upload_to_r2(filepath, r2_config):
    """上传到 Cloudflare R2(S3 兼容 API)"""
    client = boto3.client(
        's3',
        endpoint_url=r2_config['endpoint'],
        aws_access_key_id=r2_config['access_key'],
        aws_secret_access_key=r2_config['secret_key'],
    )

    remote_name = os.path.basename(filepath)
    client.upload_file(filepath, r2_config['bucket'], remote_name)

五、通知推送:钉钉/微信/Telegram

5.1 钉钉机器人

import requests
import json

def notify_dingtalk(webhook_url, title, content):
    """发送钉钉通知"""
    payload = {
        'msgtype': 'markdown',
        'markdown': {
            'title': title,
            'text': f'## {title}\n\n{content}'
        }
    }
    resp = requests.post(webhook_url, json=payload)
    return resp.json()


# 在下载脚本末尾调用
def send_summary(webhook_url, report):
    """发送下载摘要到钉钉"""
    if report['total'] == 0:
        text = '本次无新视频'
    else:
        text = f'共下载 {report["total"]} 个视频\n'
        text += f'总大小: {report["total_size_mb"]} MB\n\n'
        for f in report['files']:
            text += f'- {f["name"]} ({f["size_mb"]} MB)\n'

    notify_dingtalk(
        webhook_url,
        f'视频下载报告 - {datetime.now().strftime("%m-%d %H:%M")}',
        text
    )

5.2 Telegram Bot

def notify_telegram(bot_token, chat_id, text):
    """发送 Telegram 通知"""
    url = f'https://api.telegram.org/bot{bot_token}/sendMessage'
    resp = requests.post(url, json={
        'chat_id': chat_id,
        'text': text,
        'parse_mode': 'Markdown',
    })
    return resp.json()

5.3 企业微信 / Server 酱

# Server 酱(微信推送,最简单)
def notify_serverchan(send_key, title, content):
    url = f'https://sctapi.ftqq.com/{send_key}.send'
    resp = requests.post(url, data={
        'title': title,
        'desp': content,
    })
    return resp.json()

六、监控频道更新:比 RSS 更可靠

很多视频平台不提供 RSS,或者 RSS 有延迟。yt-dlp 可以直接扫描频道页面,比 RSS 更快更可靠。

6.1 多平台监控

{
  "YouTube-科技": [
    "https://www.youtube.com/@Fireship/videos",
    "https://www.youtube.com/@ThePrimeagen/videos"
  ],
  "B站-教程": [
    "https://space.bilibili.com/123456/video"
  ],
  "抖音-关注": [
    "https://www.douyin.com/user/xxx"
  ]
}

6.2 只获取信息不下载(轻量监控)

# 只获取最近 5 个视频的信息,不下载
yt-dlp --dump-json --playlist-end 5 --flat-playlist \
  "https://www.youtube.com/@Fireship/videos"

七、安全注意事项

7.1 Cookie 安全

❌ 不要:
  - 把 cookies.txt 直接提交到仓库
  - 在脚本里硬编码 Cookie 值
  - 在 Actions 日志中打印 Cookie

✅ 应该:
  - 用 GitHub Secrets 存储 Cookie
  - 在脚本运行时从环境变量读取
  - 使用临时文件写入,用完后删除

7.2 定时频率控制

# 合理的定时频率
on:
  schedule:
    # YouTube 频道:每 2-6 小时一次(更新频率低)
    - cron: '0 */4 * * *'
    # 不要每分钟一次!会被 GitHub 限制,也会被平台封

7.3 避免公开仓库泄露信息

公开仓库中绝对不要出现:
- Cookie / Token / 密码
- 个人身份信息
- 视频文件的直接下载链接
- 任何违反平台 ToS 的内容

八、完整模板仓库结构

video-monitor/
├── .github/
│   └── workflows/
│       └── download.yml
├── scripts/
│   ├── download.py          # 主下载脚本
│   ├── notify.py            # 通知推送
│   └── upload.py            # 上传到云存储
├── channels.json.example    # 频道配置模板
├── archive.txt              # 下载记录(自动维护)
├── requirements.txt
└── README.md

完整工作流(包含通知)

name: 视频监控下载

on:
  schedule:
    - cron: '0 */6 * * *'
  workflow_dispatch:

jobs:
  monitor:
    runs-on: ubuntu-latest
    timeout-minutes: 45

    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'

      - name: 安装系统依赖
        run: sudo apt-get update && sudo apt-get install -y ffmpeg

      - name: 安装 Python 依赖
        run: pip install yt-dlp requests

      - name: 下载视频
        env:
          CHANNELS: ${{ secrets.CHANNELS }}
          COOKIES: ${{ secrets.COOKIES }}
        run: python scripts/download.py

      - name: 发送通知
        if: always()
        env:
          DINGTALK_WEBHOOK: ${{ secrets.DINGTALK_WEBHOOK }}
        run: python scripts/notify.py

      - name: 提交更新
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "github-actions[bot]@users.noreply.github.com"
          git add archive.txt downloads/report.json
          git diff --staged --quiet || (
            git commit -m "chore: 更新下载记录 $(date +'%Y-%m-%d %H:%M')" &&
            git push
          )

九、合规与温馨提示

  • GitHub Actions 的使用需遵守 GitHub 的服务条款,不要用于挖矿、DDoS 等滥用行为
  • 定时下载他人版权内容可能违反平台服务条款,请仅用于监控和下载自己拥有版权已获授权的内容
  • yt-dlp 的 --sleep-interval 参数可以有效降低对目标服务器的压力,请合理设置
  • Cookie 等敏感信息务必通过 GitHub Secrets 存储,不要硬编码
  • 更多讨论见 下载视频算侵权吗?聊聊个人备份与版权的那条线

用 GitHub Actions 做自动化下载,本质上就是把"定时任务"从你的电脑迁移到了 GitHub 的服务器上。免费、稳定、不需要维护,唯一的代价是花半小时写一下 YAML 配置。搭建好之后,你只需要每天打开手机看看钉钉/微信通知,知道今天又下载了哪些新视频——剩下的全自动。

本文由 VidDown 技术博客原创发布。VidDown 桌面客户端支持批量下载和定时任务——如果你不想折腾 GitHub Actions,也可以用桌面端的"定时检查更新"功能。访问 VidDown 了解更多。

想亲手试试?用 VidDown 一键解析下载

粘贴视频链接即可解析,多平台支持、网页端即用;下载桌面客户端解锁海外平台本地解析,开通会员更享不限次下载。

顶部