不用买服务器,用 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 免费额度够用吗
- 二、最小可用方案:10 分钟搭好
- 三、进阶:增量下载与去重
- 四、处理大文件:突破 100MB 限制
- 五、通知推送:钉钉/微信/Telegram
- 六、监控频道更新:比 RSS 更可靠
- 七、安全注意事项
- 八、完整模板仓库结构
- 九、合规与温馨提示
一、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 了解更多。