1. 概述
在网页上点一下按钮,服务器就检查指定频道在北京时间“今天”有没有新视频或直播回放:有就下载并转成 m4a 音频,没有就提示跳过;音频通过网址直接播放或下载。上午下载频道1,下午下载频道2。
1.1 设计要点
- 下载放在 root 的后台脚本里,不放在 PHP 请求里。 下载要几十秒到几分钟,会超过 nginx 和 CDN 的超时,还会占满 PHP 进程。
- 网页只做一件事:创建一个触发文件。 即使网页被人攻破,最多也只能触发一次下载,执行不了命令,也读不到 cookie。
- “今天有没有新视频”由 yt-dlp 读取视频的发布时间来判断,不解析网页,也不依赖 RSS。
- 不重复下载:已经下载过的视频 ID 会被记住;正在直播或尚未开始的视频会被跳过,等直播结束后再点一次即可。
1.2 前置条件
- 有 root 权限的 Linux 服务器。本文以 Debian 或 Ubuntu、nginx 加 PHP-FPM 为例,其他环境照着改路径即可。
- 服务器能访问 YouTube。需要登录或遇到验证的频道,还要一份登录 cookie(第 3 章)。
- 内存:yt-dlp 加 ffmpeg 转码时会短时占用一两百 MB(估计值,以实测为准)。1GB 内存的机器要留意交换区,下载尽量放在低峰时段。
- 文中用 example.org 作示例域名,站点目录为 /srv/sites/example.org,频道用 channel_a、channel_b 代替,请换成你自己的。
1.3 使用前请知悉
- 下载 YouTube 内容可能违反其服务条款,也涉及版权。建议仅用于个人学习,不要对外公开提供下载。
- 登录 cookie 等同于账号凭证,使用一个专门的小号,放在网站目录之外。一旦怀疑泄露,立即退出该账号的所有会话并重新导出。
- 用账号 cookie 访问 YouTube 有被限制账号的风险。
2. 原理与组成
点按钮后,网页只放下一个触发文件;真正的下载由 root 的后台脚本完成,网页再读取脚本写下的状态来显示进度。

流程 · 网页放触发文件,root 脚本负责下载
图中加粗边框的是触发文件,它是网页唯一能写入的东西;其余步骤都在 root 权限下完成。
2.1 目录与权限
| 路径 | 用途 | 属主 |
| $SITE/html/ytb.php | 网页:显示状态,点按钮时创建触发文件 | PHP 用户 |
| $SITE/ytb-trigger/go | 触发文件,唯一允许网页写入的位置 | PHP 用户 |
| $SITE/ytb-status/ | status.txt(运行进度)和已下载视频的 ID | root |
| $SITE/html/downloads/ | 音频输出,nginx 直接发送给访客 | root |
| /root/ytb/cookies.txt | YouTube cookie,放在网站目录之外 | root,权限 600 |
| /usr/local/sbin/ytb-update.sh | 下载脚本 | root,权限 700 |
$SITE 是站点目录,例如 /srv/sites/example.org。
3. 安装依赖与导出 cookie
三个程序都装在宿主机上,不要装进 PHP 的容器:yt-dlp 负责下载,ffmpeg 负责转成 m4a,deno 是 yt-dlp 解析 YouTube 页面时需要的 JavaScript 运行环境。
3.1 yt-dlp
curl -fsSL https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o /usr/local/bin/yt-dlp
chmod a+rx /usr/local/bin/yt-dlp
yt-dlp --version
YouTube 经常改版,出现读取失败时,先执行 yt-dlp -U 更新。
3.2 ffmpeg
apt install -y ffmpeg
ffmpeg -version | head -1
3.3 deno
uname -m # x86_64 用下面的文件名;aarch64 把文件名换成 deno-aarch64-unknown-linux-gnu.zip
apt install -y unzip
mkdir -p /opt/deno/bin
curl -fsSL -o /tmp/deno.zip https://github.com/denoland/deno/releases/latest/download/deno-x86_64-unknown-linux-gnu.zip
unzip -o /tmp/deno.zip -d /opt/deno/bin
chmod 755 /opt/deno/bin/deno
rm -f /tmp/deno.zip
/opt/deno/bin/deno --version
脚本里用 –js-runtimes deno:/opt/deno/bin/deno 指定它,不需要加进 PATH,也不会常驻内存。
3.4 导出 YouTube cookie
- 用一个专门的小号在浏览器里登录 YouTube。
- 按 yt-dlp 官方 wiki 里“导出 YouTube cookie”的说明,导出 Netscape 格式的 cookies.txt。导出方法和注意事项会随 YouTube 变化,以官方最新说明为准。(chrome安装Get cookies.txt LOCALLY扩展插件、firefox安装Export Cookies插件)
- 上传到服务器的 /root/ytb/cookies.txt(目录和权限见第 4 章)。
- cookie 会过期,也可能被 YouTube 轮换。脚本报“读取频道列表失败”时,先更新 yt-dlp,仍不行再重新导出 cookie。
只看公开频道、不需要登录的话,可以不用 cookie:把脚本里 YT=(…) 一行中的 –cookies “$COOKIES” 删掉即可。
4. 创建目录与设置权限
权限的原则是:只有触发目录允许网页的 PHP 用户写入,其余目录都归 root。这样即使 PHP 被攻破,也无法在以 root 运行的脚本会写入的目录里放符号链接,诱使它改写系统文件。
SITE=/srv/sites/example.org # 站点目录,换成你的
PHPUID=33 # PHP-FPM 运行用户的 uid(Debian 的 www-data 用 id www-data 查看)
mkdir -p $SITE/html/downloads $SITE/ytb-status $SITE/ytb-trigger /root/ytb
chown root:root $SITE/html/downloads $SITE/ytb-status
chmod 755 $SITE/html/downloads $SITE/ytb-status
chown $PHPUID:$PHPUID $SITE/ytb-trigger
chmod 755 $SITE/ytb-trigger
chmod 700 /root/ytb
cookie 文件放进 /root/ytb/ 之后:
chmod 600 /root/ytb/cookies.txt
两点注意:
- 以后不要对整个站点目录执行 chown -R,那会把 downloads 和 ytb-status 的属主改回普通用户,上面的隔离就失效了。
- nginx 的运行用户要能读取 downloads 里的文件,所以目录是 755、音频文件是 644(脚本会自动设置)。
5. 下载脚本
脚本检查频道列表里最新的 3 个视频,取第一个“北京时间今天发布”的;正在直播或尚未开始的会跳过;已下载过的不重复下载;下载先写到临时目录,完成后一次性替换,访客不会拿到半个文件。
保存为 /usr/local/sbin/ytb-update.sh,把开头的 SITE 和频道换成你自己的:
#!/bin/bash
# /usr/local/sbin/ytb-update.sh
# 检查频道在北京时间“今天”有没有新视频,有就下载音频。
# 用法:ytb-update.sh [am|pm] 不带参数时,北京时间 12 点前用 am,之后用 pm
set -uo pipefail
TZONE=Asia/Shanghai
SITE=/srv/sites/example.org
DIR=$SITE/html/downloads # 音频输出目录(root 所有)
STATE=$SITE/ytb-status # 状态目录(root 所有)
TRIGGER=$SITE/ytb-trigger/go # 触发文件(唯一允许 PHP 写入的位置)
COOKIES=/root/ytb/cookies.txt
YTDLP=/usr/local/bin/yt-dlp
DENO=/opt/deno/bin/deno
YT=(--cookies "$COOKIES" --js-runtimes "deno:$DENO")
# 频道路径 输出文件名 (路径 = 频道名(@ 后面的部分)+ /videos 或 /streams)
CHANNELS_AM=("channel_a/videos 1.m4a")
CHANNELS_PM=("channel_b/streams 2.m4a")
HOUR=$((10#$(TZ=$TZONE date +%H))) #把“#”后面的数字作为十进制
MODE=${1:-auto} #如果$1(命令行第1个参数)未设置或空,则把auto赋给MODE
if [ "$MODE" = auto ]; then
if [ "$HOUR" -lt 12 ]; then MODE=am; else MODE=pm; fi
fi
case "$MODE" in
am) CHANNELS=("${CHANNELS_AM[@]}") ;;
pm) CHANNELS=("${CHANNELS_PM[@]}") ;;
*) echo "用法: $0 [am|pm]"; exit 1 ;;
esac
rm -f "$TRIGGER"
exec 9>/var/lock/ytb-update.lock
flock -n 9 || exit 0 # 已有任务在跑(拿不到锁),本次触发直接忽略
: > "$STATE/status.txt" # 清空/创建空文件
TODAY=$(TZ=$TZONE date +%F)
echo "今天(北京时间): $TODAY,模式: $MODE"
for item in "${CHANNELS[@]}"; do
read -r CH OUT _ <<<"$item"
[[ "$CH" =~ ^[A-Za-z0-9._-]+/(videos|streams)$ ]] || { echo "$OUT: 频道格式不对: $CH"; continue; }
# 频道页最新的 3 条
IDS=$("$YTDLP" "${YT[@]}" --flat-playlist --playlist-end 3 --print id -q \
"https://www.youtube.com/@$CH" 2>/dev/null) || IDS=""
if [ -z "$IDS" ]; then echo "$OUT: 读取频道列表失败"; continue; fi
# 找最新的一条“发布日期是今天”的(直播取开播时间)
ID=""
for vid in $IDS; do
[[ "$vid" =~ ^[A-Za-z0-9_-]{11}$ ]] || continue
META=$("$YTDLP" "${YT[@]}" --skip-download -q --no-warnings \
--print '%(release_timestamp,timestamp)s %(live_status)s' \
"https://www.youtube.com/watch?v=$vid" 2>/dev/null) || continue
read -r TS LIVE <<<"$META"
[[ "$TS" =~ ^[0-9]+$ ]] || continue
[ "$(TZ=$TZONE date -d "@$TS" +%F)" = "$TODAY" ] || continue
if [ "$LIVE" = "is_live" ] || [ "$LIVE" = "is_upcoming" ]; then
echo "$OUT: $vid 正在直播或尚未开始,跳过"; continue
fi
ID=$vid; break
done
if [ -z "$ID" ]; then echo "$OUT: 今天没有可下载的视频,跳过"; continue; fi
LAST="$STATE/$OUT.id"
if [ -f "$DIR/$OUT" ] && [ "$(cat "$LAST" 2>/dev/null)" = "$ID" ]; then
echo "$OUT: 今天的视频 $ID 已经下载过,跳过"; continue
fi
echo "$OUT: 开始下载 $ID ..."
TMP=$(mktemp -d -p "$DIR" .tmp.XXXXXX) #创建临时目录$DIR/.tmp.XXXXXX
nice -n 10 "$YTDLP" "${YT[@]}" -x --audio-format m4a -f 'wa[ext=m4a]/wa' --audio-quality 9 \
-q -o "$TMP/audio.%(ext)s" "https://www.youtube.com/watch?v=$ID" \
|| echo "$OUT: yt-dlp 返回错误" #以低优先级运行命令
if [ -s "$TMP/audio.m4a" ]; then #文件存在且大小大于0
chmod 644 "$TMP/audio.m4a"
mv -f "$TMP/audio.m4a" "$DIR/$OUT"
echo "$ID" > "$LAST"
echo "$OUT: 完成 ($(du -h "$DIR/$OUT" | cut -f1))"
else
echo "$OUT: 没有得到音频文件,稍后再试"
fi
rm -rf "$TMP"
done
echo "全部完成 $(TZ=$TZONE date '+%F %T')"
chmod 700 /usr/local/sbin/ytb-update.sh
bash -n /usr/local/sbin/ytb-update.sh && echo 语法OK
5.1 可以调整的地方
- 频道路径:就是浏览器里频道页网址 https://www.youtube.com/@ 后面的部分。视频页加 /videos,直播回放页加 /streams。
- 输出文件名固定,每天覆盖前一天的文件;想保留历史,把 $OUT 换成带日期的名字。
- 时段:上午只查 CHANNELS_AM,下午只查 CHANNELS_PM。只需要一个时段时,把另一个数组留空 ();不想区分时段,把所有频道放进两个数组,或自己去掉 case 那一段。分界点 12 点和时区都在脚本开头改。
- 一天发了多条时,取频道页最新 3 条里第一个今天的。要更多就调大 –playlist-end。
- 音频格式:-f ‘wa[ext=m4a]/wa’ 取体积最小的音轨(–audio-quality 9 也是最低音质档,想要更好的音质就把它改成 0),想要更高音质把 wa 换成 ba。
6. 手动验证
先不接网页,在服务器上手动跑通脚本,再往下做。
第一步:确认 yt-dlp 能读到最新视频和发布时间(把 channel_b/streams 换成你的频道):
Y="/usr/local/bin/yt-dlp --cookies /root/ytb/cookies.txt --js-runtimes deno:/opt/deno/bin/deno"
ID=$($Y --flat-playlist --playlist-end 1 --print id -q https://www.youtube.com/@channel_b/streams); echo "ID=$ID"
$Y --skip-download -q --print '%(release_timestamp,timestamp)s %(live_status)s' "https://www.youtube.com/watch?v=$ID"
第二条命令应输出一串数字加一个状态,例如 1761187200 was_live。如果 ID 为空,多半是 cookie 过期、yt-dlp 太旧或缺少 deno;如果时间戳是 NA,脚本无法判断“今天”,先解决这个问题。
第二步:运行脚本
/usr/local/sbin/ytb-update.sh pm # 只处理 CHANNELS_PM;am 则处理 CHANNELS_AM
ls -lh /srv/sites/example.org/html/downloads/
输出类似:
今天(北京时间): 2026-10-29,模式: pm
2.m4a: 开始下载 xxxxxxxxxxx ...
2.m4a: 完成 (18M)
全部完成 2026-10-29 16:20:41
今天没有视频时,输出“今天没有可下载的视频,跳过”,不会创建文件,这是正常的。
第三步:再运行一次,应看到“今天的视频 … 已经下载过,跳过”,说明不会重复下载。
7. 用网页触发
网页只创建一个触发文件,root 的 systemd 发现它就运行脚本,网页再读取脚本写下的状态来显示进度。整个下载过程中,网页进程不执行任何命令。
7.1 systemd:发现触发文件就运行脚本
systemd 的单元文件不能使用 shell 变量,下面的 example.org 路径要手工替换。
编辑文件 /etc/systemd/system/ytb-update.service:
[Unit]
Description=Download today's YouTube audio
[Service]
Type=oneshot
ExecStart=/usr/local/sbin/ytb-update.sh
StandardOutput=append:/srv/sites/example.org/ytb-status/status.txt
StandardError=append:/srv/sites/example.org/ytb-status/status.txt
TimeoutStartSec=3600
Nice=10
MemoryMax=400M
MemoryMax=400M 是保护措施:下载进程失控时只杀它自己,不拖垮整台机器。内存充裕的机器可以调大,也可以删掉这一行。
编辑文件 /etc/systemd/system/ytb-update.path:
[Unit]
Description=Watch trigger file for ytb-update
[Path]
PathExists=/srv/sites/example.org/ytb-trigger/go
Unit=ytb-update.service
[Install]
WantedBy=multi-user.target
运行以下几个命令激活服务ytb-update.path:
systemctl daemon-reload
systemctl enable --now ytb-update.path
systemctl status ytb-update.path --no-pager | head -5 # 应为 active (waiting)
7.2 nginx:基本认证与下载目录
先建密码文件。用户名是命令里冒号前面的 ytb(想换名字就改这里),密码是命令提示你输入的那个,要输入两次。浏览器访问时弹出的登录框就用这一对。再确认 nginx 的运行用户能读取该文件:
printf 'ytb:%s\n' "$(openssl passwd -apr1)" > /etc/nginx/ytb.htpasswd
chmod 640 /etc/nginx/ytb.htpasswd && chown root:www-data /etc/nginx/ytb.htpasswd
把下面的 location 加到站点的 server { } 里。fastcgi_pass 换成你自己的 PHP-FPM 地址;如果开了 FastCGI 页面缓存,这个 location 里不要写缓存指令:
location = /ytb.php {
auth_basic "private";
auth_basic_user_file /etc/nginx/ytb.htpasswd;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root/ytb.php;
fastcgi_pass unix:/run/php/php8.3-fpm.sock;
fastcgi_read_timeout 30s;
}
location /downloads/ {
auth_basic "private";
auth_basic_user_file /etc/nginx/ytb.htpasswd;
}
# 以点开头的文件(下载时的临时目录)不允许访问
location ~ /\.(?!well-known) { deny all; }
注意:
location中的开头三行的作用是:当浏览/ytb.php或者/downloads/时,触发nginx不把请求交给php,而直接返回401 Unauthorized,并带上一个响应头 WWW-Authenticate: Basic realm="private";浏览器看到这个头,就弹出自带的登录对话框,提示输入用户名和密码;最后浏览器把用户名和密码编码后放进请求头 Authorization 再请求一次;nginx 拿它和 /etc/nginx/ytb.htpasswd 里存的用户名、密码哈希比对,对了才放行给 PHP,页面才会出来。“private”是弹窗里显示的文字,可随便修改。
重载nginx服务:
nginx -t && systemctl reload nginx
音频文件由 nginx 直接发送,不经过 PHP,所以播放器可以拖动进度条。
7.3 网页 ytb.php
保存为 $SITE/html/ytb.php,属主为 PHP 用户,权限 644。它只接受来自本站页面的 POST,并只做一件事:创建触发文件。要显示的文件名和频道名改开头的 $names;如果 PHP 设置了 open_basedir,要包含整个站点目录(html 的上一级)。
<?php
declare(strict_types=1);
date_default_timezone_set('Asia/Shanghai');
header('Cache-Control: no-store');
$base = dirname(__DIR__); // 站点目录,例如 /srv/sites/example.org
$trigger = $base . '/ytb-trigger/go';
$status = $base . '/ytb-status/status.txt';
$dl = __DIR__ . '/downloads';
$names = ['1.m4a' => 'channel_a', '2.m4a' => 'channel_b']; // 文件名 => 显示名
// 只接受来自本站页面的 POST
if (($_SERVER['REQUEST_METHOD'] ?? '') === 'POST') {
$origin = parse_url($_SERVER['HTTP_ORIGIN'] ?? '', PHP_URL_HOST);
if ($origin === null || $origin !== ($_SERVER['HTTP_HOST'] ?? '')) {
http_response_code(403);
exit('forbidden');
}
@touch($trigger);
header('Location: /ytb.php', true, 303);
exit;
}
$text = is_file($status) ? (string)file_get_contents($status) : '';
$statusAge = is_file($status) ? time() - (int)filemtime($status) : PHP_INT_MAX;
$trigAge = is_file($trigger) ? time() - (int)filemtime($trigger) : -1;
$stuck = $trigAge > 180; // 触发文件三分钟没被处理
$running = ($trigAge >= 0 && !$stuck)
|| ($text !== '' && $statusAge < 1800 && strpos($text, '全部完成') === false);
$h = fn(string $s): string => htmlspecialchars($s, ENT_QUOTES, 'UTF-8');
?>
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!--页面每隔5秒钟刷新一次-->
<?php if ($running): ?><meta http-equiv="refresh" content="5"><?php endif; ?>
<title>音频下载</title>
<style>
body{font:16px/1.6 system-ui,sans-serif;max-width:640px;margin:2rem auto;padding:0 1rem}
button{font-size:1rem;padding:.6rem 1.2rem}
pre{background:#f4f4f4;padding:1rem;overflow:auto;white-space:pre-wrap}
</style>
</head>
<body>
<h1>音频下载</h1>
<p>今天(北京时间):<?= $h(date('Y-m-d')) ?></p>
<form method="post" action="/ytb.php">
<button type="submit"<?= $running ? ' disabled' : '' ?>>检查并下载今天的视频</button>
</form>
<?php if ($stuck): ?>
<p><strong>触发文件超过 2 分钟没有被处理,请在服务器上检查 ytb-update.path。</strong></p>
<?php endif; ?>
<p><?= $running ? '运行中,页面每 5 秒自动刷新……' : '空闲' ?></p>
<pre><?= $h($text !== '' ? $text : '还没有运行记录') ?></pre>
<h2>文件</h2>
<ul>
<?php $any = false; foreach ($names as $f => $label): $p = $dl . '/' . $f; if (is_file($p)): $any = true; ?>
<li><a href="/downloads/<?= $h($f) ?>"><?= $h($label) ?>(<?= $h($f) ?>)</a>
<?= $h((string)round(filesize($p) / 1048576, 1)) ?> MB,<?= $h(date('m-d H:i', (int)filemtime($p))) ?></li>
<?php endif; endforeach; if (!$any): ?>
<li>还没有文件</li>
<?php endif; ?>
</ul>
</body>
</html>
修改主人和权限:
chown $PHPUID:$PHPUID $SITE/html/ytb.php && chmod 644 $SITE/html/ytb.php
7.4 测试
curl -sI https://www.example.org/ytb.php | head -1 # 应为 401(要密码)
curl -sI -u ytb https://www.example.org/ytb.php | head -1 # 提示输入密码后应为 200
然后在浏览器打开 https://www.example.org/ytb.php,浏览器会弹出登录对话框(这是浏览器自带的,不是页面上的输入框),输入用户名 ytb 和刚才设的密码,登录后点按钮。几秒内页面应显示“运行中”,随后出现脚本的输出,最后一行是“全部完成”。服务器上也可以看:
journalctl -u ytb-update.service -n 20 --no-pager
cat /srv/sites/example.org/ytb-status/status.txt
8. Cloudflare(可选)
域名没有走 Cloudflare 的代理(橙云)时,跳过本章。走了的话,要让 /ytb.php 和 /downloads/ 绕过缓存,否则状态页会过期,每天被同名新文件替换的音频也可能拿到旧的。
8.1 绕过缓存
在 Cloudflare 控制台选中域名,进入 Caching → Cache Rules → Create rule:
- 规则名随意,例如 bypass ytb and downloads。
- 选 Custom filter expression,点 Edit expression,粘贴:
(http.request.uri.path eq "/ytb.php") or (starts_with(http.request.uri.path, "/downloads/"))
- Cache eligibility 选 Bypass cache,点 Deploy。
几十秒后验证(认证通过后):
curl -sI -u ytb https://www.example.org/ytb.php | grep -i cf-cache-status
curl -sI -u ytb https://www.example.org/downloads/1.m4a | grep -i cf-cache-status
应该是 BYPASS 或 DYNAMIC,不应出现 HIT。控制台的菜单名称会随改版变化,以实际界面为准。
8.2 关于 Cloudflare 的使用条款
Cloudflare 的 CDN 条款限制免费、专业和商业套餐通过代理提供视频,或占比过大的音频等大文件,违反时内容可能被重定向。自用的小流量问题不大,但不要把它当作音频分发站。通过 Cloudflare Tunnel 对外提供服务同样要走这套条款。详见 Cloudflare 官方说明。
9. 日常使用与维护
9.1 使用
- 浏览器打开 https://www.example.org/ytb.php,在浏览器弹出的登录框里输入用户名 ytb 和密码,点“检查并下载今天的视频”。北京时间 12 点前只处理 CHANNELS_AM,12 点后只处理 CHANNELS_PM。
- 当天晚些才发布的视频,到时再点一次。直播回放要等直播结束后才能下载。
- 播放或下载:页面下方的文件链接,或直接访问 /downloads/1.m4a。因为加了密码,部分播放器需要用 https://用户名:密码@www.example.org/downloads/1.m4a 这种写法。
- 也可以在服务器上手动运行:/usr/local/sbin/ytb-update.sh am 或 pm。
9.2 维护
| 什么时候 | 做什么 | 命令 |
| 每月一次,或脚本报“读取频道列表失败” | 更新 yt-dlp | yt-dlp -U |
| 同上仍失败 | 重新导出 cookie,覆盖 /root/ytb/cookies.txt | 见 3.4 |
| 更新 yt-dlp 或 deno 之后 | 手动运行一次脚本,确认仍然正常 | /usr/local/sbin/ytb-update.sh pm |
| 每季度 | 查看磁盘占用(文件每天覆盖,占用很小) | du -sh $SITE/html/downloads |
| 要改访问密码 | 重新生成密码文件,nginx 不需要重载 | printf ‘ytb:%s\n’ “$(openssl passwd -apr1)” > /etc/nginx/ytb.htpasswd |
10. 常见问题排查
| 现象 | 常见原因 | 处理 |
| 打开 ytb.php 没有弹出登录框,直接看到页面 | 7.2 节的 location 没加对,或没重载 nginx;也可能浏览器已保存过密码 | 执行 curl -sI https://你的域名/ytb.php | head -3,应为 401 并带 www-authenticate: Basic;不是的话检查 location 是否在正确的 server 块里,再 nginx -t && systemctl reload nginx,并换无痕窗口再试 |
| 登录框反复弹出 | 用户名或密码不对 | 用户名是 ytb,不是邮箱;忘了密码就重新生成 7.2 节的密码文件 |
| 页面提示“触发文件超过 2 分钟没有被处理” | ytb-update.path 没启动,或单元里的路径写错 | systemctl status ytb-update.path,确认路径与 SITE 一致 |
| 页面一直显示“运行中” | 脚本卡住,或被中途杀掉、没有写出“全部完成” | journalctl -u ytb-update.service -n 50;状态文件超过 30 分钟没更新,页面会恢复为空闲 |
| “读取频道列表失败” | cookie 过期、yt-dlp 太旧、缺 deno,或频道路径写错 | yt-dlp -U;重新导出 cookie;/opt/deno/bin/deno –version;检查路径是否带 /videos 或 /streams |
| “今天没有可下载的视频,跳过” | 确实没有;发布时间不是北京时间今天;直播还没结束 | 稍后再点;用第 6 章第一步的命令查看时间戳和状态 |
| 时间戳显示 NA | yt-dlp 取不到发布时间 | 更新 yt-dlp,再手动运行第 6 章第一步 |
| 下载失败,提示找不到 ffmpeg | 没装 ffmpeg | apt install -y ffmpeg |
| 下载时机器变得很卡,或脚本被杀 | 内存不足,或超过 MemoryMax | 加交换区,调大 MemoryMax,把下载放在低峰时段 |
| 点按钮提示 forbidden | 请求的来源域名与访问的域名不一致,例如经过了改写域名的代理 | 用站点的正式域名访问 |
| 播放器不能拖动进度条 | 音频被 PHP 或别的程序转发了 | 确认 /downloads/ 由 nginx 直接发送静态文件 |
| 访问音频 404 | 今天还没有下载成功,或文件名与脚本里的不一致 | 看页面状态区,ls $SITE/html/downloads |
补充:
youtube改版了,下线了 48kbps 的低音质 m4a,改为128kbps,所以市场1:30左右的视频,音频文件大了近三倍。建议直接拥抱 Opus 格式(最快,不损耗 CPU),Opus 格式在 50kbps 下的音质远好于同码率的 m4a,且无需重新编码,瞬间就能完成提取。唯一的缺点是部分老旧播放器可能不支持 .opus,但浏览器支持。因此下载命令可为:
/usr/local/bin/yt-dlp --cookies /root/ytb/cookies.txt --js-runtimes deno:/opt/deno/bin/deno -x --audio-format opus -f wa -q -o "/tmp/2.%(ext)s" https://www.youtube.com/watch?v=H-aa-66kz4I
即音频文件为/tmp/2.opus,文件尺寸略大于m4a。