init
This commit is contained in:
+38
@@ -0,0 +1,38 @@
|
||||
# 命令行使用方法
|
||||
命令行模式需要运行`acfunlive -listen`,下载的视频和弹幕默认保存在本程序所在文件夹内
|
||||
|
||||
`acfunlive -listen` 运行程序监听,监听过程中可以输入命令修改设置(运行`help`查看详细命令说明)
|
||||
|
||||
`acfunlive -listen -config configDir -record recordDir` 运行程序监听,读取`configDir`里的配置文件,并将录播和弹幕文件保存在`recordDir`
|
||||
|
||||
`acfunlive -webui` 启动 web UI 服务器,可以通过`http://localhost:51890`访问 web UI 界面
|
||||
|
||||
`acfunlive -webapi` 运行监听程序并启动 web API 服务器,可以通过`http://localhost:51880`来查看状态和发送命令
|
||||
|
||||
`acfunlive -mirai` 利用 Mirai 发送直播通知到指定 QQ 或 QQ 群
|
||||
|
||||
`acfunlive -danmuburn xxx.jsonl -video xxx.flv` 把弹幕合成到视频里,不指定`-video`时只输出带头像的弹幕层。弹幕文件也接受上游原版的`.ass`(会先还原成 jsonl 再合并),可以用`-out`、`-offset`、`-vbitrate`、`-ffmpeg`分别指定输出文件、时间偏移、目标码率和 ffmpeg 的位置
|
||||
|
||||
`acfunlive -listlive` 列出正在直播的主播
|
||||
|
||||
`acfunlive -addnotify 23682490` 通知 uid 为 23682490 的主播的开播
|
||||
|
||||
`acfunlive -delnotify 23682490` 取消通知 uid 为 23682490 的主播的开播
|
||||
|
||||
`acfunlive -addrecord 23682490` uid 为 23682490 的主播直播时自动下载其直播视频
|
||||
|
||||
`acfunlive -delrecord 23682490` 取消自动下载 uid 为 23682490 的主播的直播视频
|
||||
|
||||
`acfunlive -adddanmu 23682490` uid 为 23682490 的主播直播时自动下载其直播弹幕
|
||||
|
||||
`acfunlive -deldanmu 23682490` 取消自动下载 uid 为 23682490 的主播的直播弹幕
|
||||
|
||||
`acfunlive -getdlurl 23682490` 查看 uid 为 23682490 的主播是否在直播,输出其直播源
|
||||
|
||||
`acfunlive -startrecord 23682490` 临时下载 uid 为 23682490 的主播的直播视频
|
||||
|
||||
`acfunlive -startdanmu 23682490` 临时下载 uid 为 23682490 的主播的直播弹幕
|
||||
|
||||
`acfunlive -startrecdan 23682490` 临时下载 uid 为 23682490 的主播的直播视频和弹幕
|
||||
|
||||
运行`acfunlive -h`查看详细设置说明
|
||||
@@ -0,0 +1,124 @@
|
||||
"""AcFun 直播录制通知接收插件(供 acfunlive 的「通知推送」功能使用)
|
||||
|
||||
acfunlive 会把开播 / 下播 / 开始录制 / 合并完成等事件以 JSON POST 过来,
|
||||
本插件把它们转成群消息发出去。
|
||||
|
||||
安装:
|
||||
把本文件复制到 Momoko 的 plugins 目录(和 upload_notify.py 放一起),重启 bot。
|
||||
|
||||
acfunlive 侧填写(web UI 的「通知推送地址」):
|
||||
http://127.0.0.1:3000/acfunlive/notify
|
||||
(端口换成你 bot.py 里 nonebot.run(port=...) 的实际端口)
|
||||
|
||||
可选环境变量(写在 bot 的 .env 里):
|
||||
ACFUNLIVE_NOTIFY_GROUP=777163254 # 默认通知群,多个用逗号分隔;补底用,见下
|
||||
ACFUNLIVE_NOTIFY_TOKEN=某个口令 # 填了之后会校验 acfunlive 推送时带的 Bearer 口令
|
||||
|
||||
通知发到哪个群:优先用 acfunlive 推送里带的 group 字段(在 acfunlive 的 web UI
|
||||
「通知群号」里配,改完立即生效,不用重启 bot);没配时才回退到环境变量
|
||||
ACFUNLIVE_NOTIFY_GROUP;两边都没配就只记日志不发送。
|
||||
|
||||
acfunlive 推送的 JSON 字段:
|
||||
event liveOn / liveOff / recordStart / mergeDone / test
|
||||
uid 主播 uid
|
||||
name 主播名字
|
||||
title 直播标题
|
||||
url 直播间地址
|
||||
file 相关文件路径(录制开始时的视频、合并完成时的成片)
|
||||
group 通知群号(acfunlive 页面里配的)
|
||||
text 已经拼好的文本,可以直接发出去
|
||||
time 事件发生的时刻
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from nonebot import get_bot, get_driver
|
||||
from nonebot.log import logger
|
||||
|
||||
driver = get_driver()
|
||||
app: FastAPI = driver.server_app
|
||||
|
||||
# 兜底的通知群,多个用逗号分隔;acfunlive 推送里带 group 时以推送的为准
|
||||
GROUPS = [g.strip() for g in os.getenv("ACFUNLIVE_NOTIFY_GROUP", "").split(",") if g.strip()]
|
||||
# 推送口令,留空表示不校验
|
||||
TOKEN = os.getenv("ACFUNLIVE_NOTIFY_TOKEN", "").strip()
|
||||
|
||||
|
||||
def resolve_groups(data: dict) -> list[str]:
|
||||
"""决定这条通知发到哪些群:优先用推送里下发的群号,其次用环境变量"""
|
||||
raw = str(data.get("group") or "").strip()
|
||||
if not raw:
|
||||
return GROUPS
|
||||
return [g.strip() for g in raw.split(",") if g.strip()]
|
||||
|
||||
# 每种事件的前缀,让群里一眼能看出是什么事
|
||||
EVENT_PREFIX = {
|
||||
"liveOn": "🔴 开播",
|
||||
"liveOff": "⚪ 下播",
|
||||
"recordStart": "⏺ 开始录制",
|
||||
"mergeDone": "✅ 合并完成",
|
||||
"test": "🧪 测试通知",
|
||||
}
|
||||
|
||||
|
||||
def build_message(data: dict) -> str:
|
||||
"""把推送内容拼成一条群消息"""
|
||||
event = str(data.get("event", ""))
|
||||
prefix = EVENT_PREFIX.get(event, "📢 " + (event or "通知"))
|
||||
|
||||
# text 是 acfunlive 拼好的正文,优先用它
|
||||
body = str(data.get("text") or "").strip()
|
||||
if not body:
|
||||
parts = []
|
||||
if data.get("name"):
|
||||
parts.append(str(data["name"]))
|
||||
if data.get("title"):
|
||||
parts.append(str(data["title"]))
|
||||
if data.get("url"):
|
||||
parts.append(str(data["url"]))
|
||||
body = "\n".join(parts)
|
||||
|
||||
lines = [prefix]
|
||||
if body:
|
||||
lines.append(body)
|
||||
if data.get("time"):
|
||||
lines.append(str(data["time"]))
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
@app.post("/acfunlive/notify")
|
||||
async def acfunlive_notify(data: dict, request: Request):
|
||||
"""接收 acfunlive 推送的直播事件"""
|
||||
if TOKEN:
|
||||
auth = request.headers.get("authorization", "")
|
||||
got = auth[7:].strip() if auth.lower().startswith("bearer ") else auth.strip()
|
||||
if got != TOKEN:
|
||||
logger.warning("AcFun 通知的口令不正确,已忽略")
|
||||
return {"status": "error", "message": "口令不正确"}
|
||||
|
||||
message = build_message(data)
|
||||
groups = resolve_groups(data)
|
||||
logger.info(f"收到 AcFun 通知:{data.get('event')} {data.get('name', '')}")
|
||||
|
||||
if not groups:
|
||||
logger.warning("推送里没有 group,也没有设置 ACFUNLIVE_NOTIFY_GROUP,只记录日志不发送")
|
||||
return {"status": "success", "message": "未配置群号,已忽略"}
|
||||
|
||||
ok, failed = 0, []
|
||||
for group_id in groups:
|
||||
try:
|
||||
bot = get_bot()
|
||||
await bot.send_group_msg(group_id=int(group_id), message=message)
|
||||
ok += 1
|
||||
except Exception as e:
|
||||
failed.append(f"{group_id}: {e}")
|
||||
logger.error(f"向群 {group_id} 发送 AcFun 通知失败:{e}")
|
||||
|
||||
if failed:
|
||||
return {"status": "error", "message": ";".join(failed)}
|
||||
return {"status": "success", "message": f"已发送到 {ok} 个群"}
|
||||
|
||||
|
||||
logger.info("AcFun 直播通知插件加载成功")
|
||||
logger.info(f"HTTP 端点:POST /acfunlive/notify,默认群(推送里带 group 时以推送为准):{GROUPS or '未配置'}")
|
||||
@@ -0,0 +1,159 @@
|
||||
# acfunlive 源码审阅报告
|
||||
|
||||
- 日期:2026-09-25
|
||||
- 范围:13 个 Go 文件共 3735 行(config/webui/danmu/danmuburn/record/automerge/cycle/fetch/main/utils/handle/input/filename),外加 config.json、Dockerfile、README
|
||||
- 方法:逐文件通读 + 行级取证 + `go vet`(无输出)+ 关键假设实测(Windows 命令行长度上限、进程上限)
|
||||
- 结论:**本次未修改任何代码**。P0 3 项、P1 9 项、P2 16 项,其中 P0-3 已用实测数据证明"长直播的自动合并必然失败"
|
||||
|
||||
### 运行环境发现(非代码问题,但优先级很高)
|
||||
|
||||
- 本机同时存在**两个录制程序实例**:
|
||||
- `acfunlive-danmu.exe`(PID 14616,本改造版),监听 `0.0.0.0:51881`;
|
||||
- `acfunlive`(PID 10708,**上游原版**,有 `/listlive` `/startmirai` 等接口),监听 `0.0.0.0:51880`。
|
||||
- 判定依据:`netstat -ano` 显示 51880 → PID 10708、51881 → PID 14616;`GET http://127.0.0.1:51880/` 返回上游帮助文本(`/listlive`、`/startwebui`…),而 `/api/status` 返回 404。
|
||||
- 风险:如果两个实例的 uid 列表重叠,会对同一主播**重复下载、重复占用带宽**,产物互相覆盖或重复;上游版的 web API 同样是**无鉴权的写接口**(`/addnotifyon/...`),且监听所有网卡。
|
||||
- 建议:确认后关掉不需要的那一个;若上游版仍需保留,把它的 web 监听限制在本机。
|
||||
|
||||
---
|
||||
|
||||
## 1. 结论速览
|
||||
|
||||
| 级别 | 数量 | 主题 | 最值得先做的一件事 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 安全/数据 | 3 | 无认证的 web 管理面、合并 goroutine 崩溃会杀掉整个录制进程、合并命令超命令行上限 | 给管理接口加鉴权 + 合并改分块渲染 |
|
||||
| P1 正确性/竞态 | 9 | 数据竞态、硬杀 ffmpeg 导致文件损坏、合并静默丢失、并发合并字幕错配、停止操作卡 30 秒 | 修 `removeUID` 竞态、优雅停止、临时文件唯一化 |
|
||||
| P2 健壮性/体验/工程 | 16 | 配置错误直接退出、无信号处理、锁内 IO、无超时、日志不落盘等 | 统一 goroutine 兜底 + 配置降级 |
|
||||
|
||||
---
|
||||
|
||||
## 2. P0:安全与数据完整性
|
||||
|
||||
### P0-1 web UI 无任何认证,且当前配置已对外开放 🔴
|
||||
|
||||
- 证据:`webui.go:546-553` 注册的路由(`POST /api/config`、`/api/streamer`、`/api/streamer/control`、`/api/streamer/remove`)全部无鉴权、无 token、无来源校验;`config.json` 中 `webExternal: true` 意味着监听 `0.0.0.0`(`webui.go:589-594`)。
|
||||
- 影响:同一局域网(或做了端口映射的机器上)任何人可以:
|
||||
- 改 `directory` / `recordSubdir` / `mergeSubdir` / `mergeOutputDir`,把录制产物写到任意已存在目录;
|
||||
- 打开 `mergeDelVideo` / `mergeDelDanmu`(这两个开关的实际效果是**删除源文件**);
|
||||
- 增删主播、停止全部录制、把码率改成 1 导致画质损毁。
|
||||
- 缓解事实:POST 接口要求 `application/json`,浏览器跨站表单会触发 CORS 预检且服务端不返回 CORS 头,因此 CSRF 难利用;但这不改变"无认证"这一根本问题。
|
||||
- 建议:
|
||||
1. 增加 `webToken`(首次启动随机生成并打印到控制台/日志),页面 `?token=` 或 `Authorization: Bearer`;
|
||||
2. 更贴合个人工具的方案:管理类接口(config/streamer/remove/control)**默认只接受来自 127.0.0.1 的请求**,`webExternal` 只用于查看状态;
|
||||
3. `mergeDelVideo`/`mergeDelDanmu` 这类破坏性开关再加一层"仅在本地控制台/带 token 时可改"。
|
||||
|
||||
### P0-2 自动合并 goroutine 没有 recover,一次 panic 会杀掉整个录制进程 🔴
|
||||
|
||||
- 证据链:`automerge.go:42` `go autoMerge(...)`(无 `defer recover`)→ `automerge.go:116` 调 `burnDanmu` → `danmuburn.go:328` `checkErr(os.MkdirAll(cacheDir, 0755))`、`danmuburn.go:369` `checkErr(png.Encode(...))`;`checkErr` 的实现是 `panic`(`utils.go:87-91`)。`runThrice`(`utils.go:94-105`)只重试**返回 error** 的调用,捕不住 panic。
|
||||
- 影响:头像缓存目录创建失败(权限/被占用/磁盘满)或 PNG 编码失败 → panic 逃出 goroutine → **Go 运行时终止整个进程**,其它主播正在进行的录制全部中断,flv 文件被留下且未移动。
|
||||
- 对比:程序里绝大多数 goroutine 都有 recover(`cycle`/`handleInput`/`getDanmu`/`initRecord`/`startWebUI`),只有 `autoMerge` 这条最"重"的路径没有。
|
||||
- 建议:① 所有 `go` 出去的入口加 `safeGo(func())` 包装(统一 recover + 日志);② `danmuburn.go` 里的 `checkErr` 换成返回 error 或就地判空返回。
|
||||
|
||||
### P0-3 弹幕条数 ≳200 时,合并命令超出 Windows 命令行上限,自动合并必然失败 🔴
|
||||
|
||||
- 证据:`danmuburn.go:221-240` 为**每一条弹幕**生成一个 overlay 片段,`danmuburn.go:247` 把它们作为**单个** `-filter_complex` 参数传给 ffmpeg。
|
||||
- 实测(本机复刻格式串):单条片段 118 字符,实测样例
|
||||
`[s1234][av56789012_3]overlay=x='1920-(1920+1258)*(t-1234.567)/10':y=726:enable='between(t,1234.567,1244.567)'[s1235];`
|
||||
- 实测(Windows CreateProcess 上限):30000 / 32000 字符 → 成功;**40000 字符 → `WinError 206 文件名或扩展名太长`**。
|
||||
|
||||
| 弹幕条数 | filter_complex 长度 | 整条命令估算 | Windows (≤32767) |
|
||||
| --- | --- | --- | --- |
|
||||
| 100 | 11.8 KB | 16.4 KB | ok |
|
||||
| 200 | 23.6 KB | 32.2 KB | 已到临界 |
|
||||
| 290 | 34.2 KB | 46.4 KB | **必然失败** |
|
||||
| 1000 | 118 KB | 130 KB | **必然失败** |
|
||||
| 10000 | 1.2 MB | 1.3 MB | **必然失败** |
|
||||
|
||||
- 影响:一场正常直播(几千到几万条弹幕)的**自动合并/命令行合成必定报错**,且报错是 `WinError 206`,完全看不出真实原因。之前实测能成功,只是因为测试用的直播间弹幕量都不到 200 条。Linux 端单参数上限 `MAX_ARG_STRLEN` 为 128 KB,约 1100 条也会踩到。
|
||||
- 附带性能问题:O(N) 条 overlay 链,每条都对整帧做一次重编码,即使不超长度,几千条时耗时与内存也不可行。
|
||||
- 建议(推荐第 2 项):
|
||||
1. **分块渲染**:按时间窗(如每 60~120 秒)切分成多个 ffmpeg 任务,每块只包含该时段出现的弹幕与用户头像,编成 mp4 分片后用 concat demuxer 合并;音频直接从原视频 `-c:a copy`。每块弹幕数可控,链长下降两个数量级。
|
||||
2. **头像预合成**:把"头像+昵称"先渲染成不透明小图(或按用户合成一张 sprite),再按用户而非按弹幕做 overlay,配合 `enable` 表达式按用户合并时间段 —— 数量从"弹幕数"降到"用户数",可再降一个数量级。
|
||||
3. 至少在超限前**主动报错并降级**(如提示用户改用分块或关闭头像),而不是把 ffmpeg 的错误原样抛出。
|
||||
|
||||
---
|
||||
|
||||
## 3. P1:正确性 / 竞态 / 数据
|
||||
|
||||
| 编号 | 问题 | 证据 | 影响 | 建议 |
|
||||
| --- | --- | --- | --- | --- |
|
||||
| P1-1 | `removeUID` 在**无锁**状态下改 `config.UIDs` | `config.go:296-319` 直接 `append`;而 `setStreamerFlags`(`config.go:342-368`)持 `streamers.Lock` 改同一切片 | 数据竞态;并发增删时可能丢 uid 或读到脏切片;`saveConfig` 又在锁外遍历它(`config.go:270-288`),并发写同一文件会互相覆盖 | 统一用 `streamers.Lock`;`saveConfig` 用独立 `configMu` 串行化 + 临时文件原子替换 |
|
||||
| P1-2 | 停止录制是**硬杀** ffmpeg,`q` 写在进程已死之后 | `record.go:176` 用 `exec.CommandContext`;`record.go:200-204` 先 `cmd.Run()` 返回,再 `io.WriteString(stdin, "q")` | 取消 ctx 时 Go 直接 `TerminateProcess`,flv 尾部无正常收尾 → 合并时报 corrupt packet(已实测到该现象);`q` 写入是无效代码 | 用 `cmd.Cancel`(写 `q`,不做 Kill)+ `cmd.WaitDelay = 5s`,超时再杀 |
|
||||
| P1-3 | 只有首段录播参与合并,断流后的分段被忽略 | `record.go:186-197` `firstRecordFile/recordStartNs` 只在为空时赋值;`record.go:221` 续段生成新文件名;`automerge.go` 只处理 `firstRecordFile` | 直播中途断一次流,**后半场视频没有弹幕**,且合并只输出首段(弹幕时间轴也对不上)→ 内容丢失 | 方案 A(小改):续段复用同一 flv 文件名,让 ffmpeg 追加/重连;方案 B(推荐):记录所有分段与各自起始偏移,合并时先 concat 所有分段再叠弹幕 |
|
||||
| P1-4 | `cycleDelKey` 60 秒清理会**静默吞掉**自动合并 | `cycle.go:106-124` 删除"既无弹幕也无录播"的 liveInfo;`record.go:205` 分段之间会把 `isRecording` 置 false 并 `time.Sleep(5s)`(`record.go:217`);`automerge.go:16-20` 找不到 key 时直接 return 且**无日志** | 录播收尾晚于弹幕超过 60 秒(或分段间隙撞上清理周期)时,自动合并永远不触发,用户只看到"没生成成片" | 保留条件放宽:有 `assFile`/`firstRecordFile`/`mergeStarted` 的项不删;清理周期改 10 分钟;`finishPart` 找不到 key 时打日志 |
|
||||
| P1-5 | `controlStreamer` 的"重新开始"会**打开原本关闭的录制** | `config.go:461-468`,前端固定传 `target:"all"`(`webui.go:196`)→ `danmu, record = restart, restart`(都置 true) | 只开弹幕的主播点一次"重新开始",录播也被打开,产生用户没要的视频文件 | restart 只对"该项原本已开启"的部分重启:`danmu = s.Danmu && restart`,或让前端按当前勾选传 target |
|
||||
| P1-6 | 并发合并时临时字幕文件**同名冲突**,可能张冠李戴 | `danmuburn.go:170` 硬编码 `acfunlive_nick_tmp.ass`,且开头 `_ = os.Remove(tmpNick)`、结尾 `defer os.Remove(tmpNick)` | 两个主播同时下播 → 两个 autoMerge 并发 → 一方删掉另一方正在用的字幕,最坏情况把 A 的弹幕烧进 B 的视频 | 临时名加唯一后缀(liveID/uid/时间戳),或改到各自独立临时目录 |
|
||||
| P1-7 | 停止操作可能卡 4~30 秒 | `record.go:102`、`danmu.go:424` 的日志里调 `longID(uid)`(`utils.go:157-159`)→ `getName` → 缓存未命中时 `tryFetchLiveInfo` 走 3 次重试(每次 10 秒超时 + 2 秒 sleep,`utils.go:94-105`) | 主播下播后点"停止",HTTP 请求要等到日志打印完才返回,web 页面像卡死;`setStreamerFlags` 里 `getName` 还会调两次(`config.go:353,366`) | 日志走缓存名字,取不到就只打 uid;`getName` 加一层短期缓存 |
|
||||
| P1-8 | web UI 每 3 秒重建主播列表 DOM | `webui.go:144` `innerHTML = rows`;`webui.go:224` `setInterval(refresh, 3000)` | 正在输入"下载码率上限"时会被重建清空(`setVal` 只保护设置区表单,不保护列表行) | 数据无变化时不重建(比较 JSON 摘要);或在 `:focus` 元素所在行跳过重建 |
|
||||
| P1-9 | 每 3 秒全量拉取日志 | `webui.go:523-526` 返回最多 1 MB 日志,`webui.go:124` 每 3 秒取一次 | 每分钟最多 20 MB 流量,手机浏览器明显吃力,日志越长越严重 | 支持增量(`since` 偏移/时间戳)+ 前端只追加 |
|
||||
|
||||
---
|
||||
|
||||
## 4. P2:健壮性、体验与工程
|
||||
|
||||
| 编号 | 问题 | 证据 / 说明 |
|
||||
| --- | --- | --- |
|
||||
| P2-1 | 配置错误直接 `os.Exit(1)`,会中断正在进行的录制 | `config.go:177-243` 大量 `os.Exit`;`config.go:610` 在 `loadNewConfig` 里也会调它。用户手改 config.json 写错一个字符 → 整个录制进程退出。建议:保留旧配置 + 告警(只有启动阶段才致命) |
|
||||
| P2-2 | 无 SIGINT/SIGTERM 处理 | 没有 `signal.Notify`;Ctrl+C 或杀进程时 ffmpeg 变孤儿继续录(上次已实测),且退出流程是 `main.go:164` 硬 `sleep 20s` |
|
||||
| P2-3 | `mainCh` 的发送可能与退出竞争 | `main.go:116` 容量 20;`config.go:527-537` `stopStreamerCycle` 持有 `sInfoMap` 锁做 `m.ch <- stop`,若无人接收会持锁阻塞(潜在死锁);main 退出后任何 `mainCh <-` 永久阻塞。建议 `select + default` 并显式从 map 删除 |
|
||||
| P2-4 | http.Server 只设了 `IdleTimeout` | `webui.go:560-564` 缺 `ReadHeaderTimeout/ReadTimeout/WriteTimeout`;请求体也没有 `http.MaxBytesReader` 与速率限制 → 慢连接可长期占用连接 |
|
||||
| P2-5 | 页面缺少安全响应头 | 可加 `X-Content-Type-Options`、`X-Frame-Options`、`Referrer-Policy`、CSP;`/api/log` 会泄露主播名、直播标题、本机路径 |
|
||||
| P2-6 | 目录名校验不够严 | `config.go:130-135` 只拒分隔符与 `".."`;未处理 Windows 保留名(CON/NUL/PRN)、结尾空格或点;`mergeOutputDir` 可为任意已存在目录 |
|
||||
| P2-7 | Linux 文件名截断可能切碎 UTF-8 | `filename_other.go:19-21` 按**字节** `filename[:250]`,多字节汉字会被截半 → 非法文件名。应按 rune 截断 |
|
||||
| P2-8 | 头像缓存无失效、无上限、位置混放 | `danmuburn.go:323-338` 缓存名是 `uid.png`,主播换头像后永远用旧图;缓存在 `record/` 目录内与成对文件混在一起;建议按 URL 摘要命名 + 放 `cache/` 子目录 + 定期清理 |
|
||||
| P2-9 | 时间轴对齐存在 1~3 秒系统误差 | `record.go:195` 在 ffmpeg **启动前**记录 `recordStartNs`;ffmpeg 建立连接与缓冲有延迟 → 弹幕整体偏早。可考虑用 flv 首个时间戳校正 |
|
||||
| P2-10 | 弹幕坐标系可能与实际分辨率不符 | `danmu.go:43-55` 用最高档**码率**猜分辨率(≥4000→1080)。竖屏直播配上≥4000 档会选到横屏坐标系 1920×1080 → libass 拉伸变形 |
|
||||
| P2-11 | `assTime` 超过 24 小时会回绕 | `danmu.go:127-133` 用 `time.Unix(0, t).UTC()` 取 `Hour()`;超长直播(>24h)时间轴错乱 |
|
||||
| P2-12 | QSV 能力每次合并都探测 | `danmuburn.go:374-379` 每次跑一次 0.5 秒的试编码;建议进程内缓存结果 |
|
||||
| P2-13 | 日志不落盘、无轮转 | 只有内存 + stdout(`webui.go:260-270`);用计划任务/nssm 后台运行时排障困难。建议同时写 `log/` 并按大小轮转 |
|
||||
| P2-14 | ffmpeg 只在 PATH 与 exeDir 查找 | `record.go:18-31`,且不校验版本/编码器能力,出错提示不够可操作 |
|
||||
| P2-15 | `initRecord`/`initDanmu` 无 recover | 由 `main.go:88-89` 与 `cycle.go:85-90` 直接 `go` 出;当前实现无 panic 点,但一旦后续改动引入 `checkErr` 就是整机崩溃。建议统一 `safeGo` 包装 |
|
||||
| P2-16 | 大量 `time.Sleep` 兜底、缺少状态同步 | `cycle.go:101` 1s、`record.go:217` 5s、`danmu.go:310/327` 5s/10s、`config.go:478` 2s、`webui.go:610` 500ms;`restartWebUI` 依赖 panic-recover 重试(`webui.go:529-537`) |
|
||||
|
||||
---
|
||||
|
||||
## 5. 优化建议(不改行为)
|
||||
|
||||
1. **配置写入串行化 + 原子替换**:`saveConfig` 是目前唯一的多写者共享资源,改为「写 `config.json.tmp` → `os.Rename`」可同时解决半截文件与并发覆盖。
|
||||
2. **锁顺序固化**:现有顺序是 `streamers → liveRooms`(如 `buildStreamers` 持前者调 `getName` 取后者)。建议在注释中固化,并把"锁内不做 IO"写成硬约束:`getTitle/getName/longID` 一律先取值后打印。
|
||||
3. **`safeGo` 统一兜底**:所有后台任务走同一入口(recover + 日志 + 可选重启),把"是否 recover"从人工记忆变成机制。
|
||||
4. **合并任务串行化**:加一个 `mergeCh`(容量 1 的 worker),避免并发合并抢 CPU/内存与临时文件冲突(同时解掉 P1-6)。
|
||||
5. **前端轮询改造**:状态接口返回 `version`(配置/列表变更摘要),前端只在变更时重建;日志改增量拉取。
|
||||
6. **`/api/status` 瘦身**:`UIDs` 字段已冗余(前端未使用),可去掉。
|
||||
7. **CI**:加 `gofmt -l`、`go vet`、`go build`(windows/linux 双平台)与 `govulncheck`;依赖里有 `acfundanmu` 等较旧的包,建议定期看 CVE。
|
||||
8. **Docker 场景目录嵌套**:`recordDir=/acfunlive/record` + `recordSubdir=record` 会变成 `/acfunlive/record/record`;建议允许 `recordSubdir` 为空表示"不建子目录",或文档说明。
|
||||
9. **录制前检查磁盘水位**:新增剩余空间检查,低水位告警/停止录制,避免写满盘导致所有录制同时失败。
|
||||
|
||||
---
|
||||
|
||||
## 6. 下一步值得做的功能
|
||||
|
||||
### 第一批:补齐稳定性(建议先做,和上面的 P0/P1 是同一批工作)
|
||||
1. 合并改为**分块渲染 + concat**,彻底解决 P0-3,顺带把 3 小时的直播合并从"不可行"变成"可接受"。
|
||||
2. **优雅停止 + 断流续录拼接**(P1-2 / P1-3):停止时 ffmpeg 正常收尾(flv 不再损坏);断流后的分段自动拼接,弹幕覆盖全场。
|
||||
3. 管理接口鉴权 + 敏感操作本地限定(P0-1)。
|
||||
|
||||
### 第二批:无人值守能力(投入小、体感提升最大)
|
||||
4. **通知推送**:开播 / 下播 / 合并完成 / 合并失败 / 磁盘不足,推送到 **NoneBot(你已有 Momoko)/ Bark / 企业微信 / Telegram**。这是长时间挂机录制最缺的一环,实现成本约百行。
|
||||
- 佐证:**上游原版本来就有开播/下播通知**(`/addnotifyon`、`/startmirai`,走 Mirai 推 QQ),本改造版在剥离 Mirai 时把这套能力一起删掉了。现在本机还跑着那个上游实例(见上文"运行环境发现")。用 NoneBot 的 OneBot HTTP API 接回来,既恢复能力又不引入 Mirai 依赖。
|
||||
5. **文件管理**:Web UI 列出 `record/`、`merged/` 中的文件(大小、时长、是否已合并),成片在线播放(Range)与下载、一键删除;显示磁盘剩余空间。
|
||||
6. **日志落盘 + 轮转**,配合计划任务/nssm 后台运行。
|
||||
|
||||
### 第三批:进阶功能
|
||||
7. **录制后处理流水线**:合并完成后自动转 H.265(省 40%~60% 空间,可与现有 `mergeBitrate` 联动)、生成缩略图、按 `{日期}/{主播}` 归档。
|
||||
8. **弹幕数据再加工**:导出 B 站 XML / lrc;生成"弹幕热度曲线",方便后续剪辑挑精彩片段。
|
||||
9. **定时/条件录制**:只在指定时段录制、按标题关键词过滤、按清晰度门槛过滤。
|
||||
10. **Web UI 实时预览**:录制中定期截图或低码率 MJPEG 预览,配合显示当前文件大小/已录时长。
|
||||
11. **多站点支持**:抽象 `LiveProvider` 接口(AcFun/B站/虎牙/抖音),是最有价值也最费力的方向。
|
||||
12. **工程化**:systemd unit / Windows 服务 + 崩溃自愈;发布构建脚本与 Docker compose 示例。
|
||||
|
||||
---
|
||||
|
||||
## 7. 建议的落地顺序
|
||||
|
||||
| 批次 | 内容 | 验收要点 |
|
||||
| --- | --- | --- |
|
||||
| 1 | P0-2 goroutine 兜底、P1-1 竞态、P1-5 restart 语义、P1-6 临时文件唯一化、P1-8/P1-9 前端 | 并发增删主播 `-race` 无告警;两个主播同时下播合并互不干扰 |
|
||||
| 2 | P1-2 优雅停止、P1-3 断流拼接、P1-4 清理条件 | 手动停止后 flv 无损坏 packet;断流一次后成片覆盖全场 |
|
||||
| 3 | P0-3 分块渲染 | 用 3000+ 条弹幕的 jsonl 实测合并成功,耗时可接受 |
|
||||
| 4 | P0-1 鉴权 | 局域网设备无 token 时打不开管理接口 |
|
||||
| 5 | 通知 + 文件管理 | 下播合并完成后 NoneBot 收到消息;Web UI 可下载成片 |
|
||||
@@ -0,0 +1,95 @@
|
||||
# acfunlive web API
|
||||
|
||||
web API 默认本地端口为 51880
|
||||
|
||||
`http://localhost:51880/listlive` 列出正在直播的主播
|
||||
|
||||
`http://localhost:51880/listrecord` 列出正在下载的直播视频
|
||||
|
||||
`http://localhost:51880/listdanmu` 列出正在下载的直播弹幕
|
||||
|
||||
`http://localhost:51880/liststreamer` 列出设置了开播提醒或自动下载直播的主播
|
||||
|
||||
`http://localhost:51880/startmirai` 利用 Mirai 发送直播通知到指定 QQ 或 QQ 群
|
||||
|
||||
`http://localhost:51880/addnotifyon/23682490` 通知 uid 为 23682490 的主播的开播
|
||||
|
||||
`http://localhost:51880/delnotifyon/23682490` 取消通知 uid 为 23682490 的主播的开播
|
||||
|
||||
`http://localhost:51880/addnotifyoff/23682490` 通知 uid 为 23682490 的主播的下播
|
||||
|
||||
`http://localhost:51880/delnotifyoff/23682490` 取消通知 uid 为 23682490 的主播的下播
|
||||
|
||||
`http://localhost:51880/addnotifyrecord/23682490` 通知 uid 为 23682490 的主播的直播视频下载
|
||||
|
||||
`http://localhost:51880/delnotifyrecord/23682490` 取消通知 uid 为 23682490 的主播的直播视频下载
|
||||
|
||||
`http://localhost:51880/addnotifydanmu/23682490` 通知 uid 为 23682490 的主播的直播弹幕下载
|
||||
|
||||
`http://localhost:51880/delnotifydanmu/23682490` 取消通知 uid 为 23682490 的主播的直播弹幕下载
|
||||
|
||||
`http://localhost:51880/addrecord/23682490` uid 为 23682490 的主播直播时自动下载其直播视频
|
||||
|
||||
`http://localhost:51880/delrecord/23682490` 取消自动下载 uid 为 23682490 的主播的直播视频
|
||||
|
||||
`http://localhost:51880/adddanmu/23682490` uid 为 23682490 的主播直播时自动下载其直播弹幕
|
||||
|
||||
`http://localhost:51880/deldanmu/23682490` 取消自动下载 uid 为 23682490 的主播的直播弹幕
|
||||
|
||||
`http://localhost:51880/addkeeponline/23682490` uid 为 23682490 的主播直播时在其直播间里挂机
|
||||
|
||||
`http://localhost:51880/delkeeponline/23682490` 取消设置在 uid 为 23682490 的主播直播时在其直播间里挂机
|
||||
|
||||
`http://localhost:51880/delconfig/23682490` 删除 uid 为 23682490 的主播的所有设置
|
||||
|
||||
`http://localhost:51880/getdlurl/23682490` 查看 uid 为 23682490 的主播是否在直播,并输出其直播源
|
||||
|
||||
`http://localhost:51880/addqq/23682490/12345` 将 uid 为 23682490 的主播的开播提醒发送到 QQ12345,需要 QQ 机器人已经添加该 QQ 为好友
|
||||
|
||||
`http://localhost:51880/delqq/23682490/12345` 取消将 uid 为 23682490 的主播的开播提醒发送到 QQ12345
|
||||
|
||||
`http://localhost:51880/cancelqq/23682490` 取消将 uid 为 23682490 的主播的开播提醒发送到任何 QQ
|
||||
|
||||
`http://localhost:51880/addqqgroup/23682490/12345` 将 uid 为 23682490 的主播的开播提醒发送到 QQ 群 12345,需要 QQ 机器人已经加入该 QQ 群,最好是管理员,会@全体成员
|
||||
|
||||
`http://localhost:51880/delqqgroup/23682490/12345` 取消将 uid 为 23682490 的主播的开播提醒发送到 QQ 群 12345
|
||||
|
||||
`http://localhost:51880/cancelqqgroup/23682490` 取消将 uid 为 23682490 的主播的开播提醒发送到任何 QQ 群
|
||||
|
||||
`http://localhost:51880/startrecord/23682490` 临时下载 uid 为 23682490 的主播的直播视频
|
||||
|
||||
`http://localhost:51880/stoprecord/23682490` 取消下载 uid 为 23682490 的主播的直播视频
|
||||
|
||||
`http://localhost:51880/startdanmu/23682490` 临时下载 uid 为 23682490 的主播的直播弹幕
|
||||
|
||||
`http://localhost:51880/stopdanmu/23682490` 取消下载 uid 为 23682490 的主播的直播弹幕
|
||||
|
||||
`http://localhost:51880/startrecdan/23682490` 临时下载 uid 为 23682490 的主播的直播视频和弹幕
|
||||
|
||||
`http://localhost:51880/stoprecdan/23682490` 取消下载 uid 为 23682490 的主播的直播视频和弹幕
|
||||
|
||||
`http://localhost:51880/log` 查看 log
|
||||
|
||||
`http://localhost:51880/quit` 退出 acfunlive 运行
|
||||
|
||||
`http://localhost:51880/help` 显示帮助信息
|
||||
|
||||
## 新版 web UI 的管理接口
|
||||
|
||||
web UI(`-webui`,端口看`webPort`)用的是另一套`/api/`接口,全部需要带管理口令:请求头`X-Auth-Token: <口令>`,或者在 URL 上带`?token=<口令>`(下载链接用后者,因为浏览器跳转带不了请求头)。跟手动合并有关的是下面两个:
|
||||
|
||||
`POST /api/merge/info` 预览一次手动合并的计划,不落盘。
|
||||
|
||||
```
|
||||
{
|
||||
"videos": ["2026-09-25 01-00-00 主播 标题.mp4", "D:/别处/随手放的视频.mp4"],
|
||||
"danmu": "2026-09-25 01-00-00 主播 标题.ass",
|
||||
"offset": 3 // 可选,不填按文件名里的录制时间推算
|
||||
}
|
||||
```
|
||||
|
||||
返回分段列表(名字、体积、时长、是否正在写入)、总时长、弹幕条数、弹幕时间范围、要用的弹幕文件和来源(`jsonl 弹幕数据` / `同名的 jsonl 弹幕数据` / `由 xxx.ass 还原`)、推算出的偏移和原因、最终输出文件名、以及提示(`warnings`)。参数不合法一律 400,并在响应体里给中文原因。
|
||||
|
||||
`POST /api/merge` 真正开始合并,请求体和上面一样,另外可以带`outName`指定输出文件名(不合法会被清理成安全的文件名,重名自动加后缀)。成功后立刻返回`{"outName":"…"}`,合并在后台跑。已经有一个手动合并任务在跑时返回 409。
|
||||
|
||||
合并状态通过`GET /api/status`查看:`mergeBusy`(是否在跑)、`mergeOutName`(当前成片名)、`mergeErr`(失败原因)、`mergeDoneAt`(上次完成时间)。合并期间成片会出现在`GET /api/files`里且`inUse`为 true,此时`POST /api/file/remove`会返回 409 拒绝删除。
|
||||
Reference in New Issue
Block a user