Compare commits
4
Commits
3c17503a21
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dadd3f7cea | ||
|
|
40f4f36aa6 | ||
|
|
2b1705a970 | ||
|
|
7ab80ec429 |
@@ -75,7 +75,7 @@ GOOS=windows GOARCH=amd64 go build -o acfunlive.exe
|
|||||||
"qualities": [] // 开播时读到的码率档位列表,缓存下来让没开播时也能显示对应关系
|
"qualities": [] // 开播时读到的码率档位列表,缓存下来让没开播时也能显示对应关系
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"directory": "", // 录制文件保存的文件夹,留空是程序运行时的 -record 文件夹
|
"directory": "", // 录播和弹幕文件的保存根目录,录制时就直接写在这里;留空用运行时的 -record 文件夹
|
||||||
"webPort": 51880, // web UI 的端口,为 0 时不启动 web UI
|
"webPort": 51880, // web UI 的端口,为 0 时不启动 web UI
|
||||||
|
|
||||||
"autoMerge": false, // 直播结束后自动把弹幕层合并进录播
|
"autoMerge": false, // 直播结束后自动把弹幕层合并进录播
|
||||||
@@ -127,7 +127,7 @@ GOOS=windows GOARCH=amd64 go build -o acfunlive.exe
|
|||||||
|
|
||||||
档位表要开播后读直播源列表才知道(写在`config.json`的`qualities`里缓存)。没读到过的行,选项后面不会带档位名,旁边的`档位`按钮可以去直播间接一次(主播没开播时会失败并提示,服务端本身要重试三次、约 5 秒,所以点下去会先给一句`正在读取码率档位…`)。读档位的时机是:录制开始取流时、页面上点`档位`按钮、以及发现主播在直播但还没有档位缓存时自动读一次(同一个主播 10 秒内只读一次)。录制始终是原画质直接保存(`-c copy`),不会重新编码,所以只能在这几个档位里挑。
|
档位表要开播后读直播源列表才知道(写在`config.json`的`qualities`里缓存)。没读到过的行,选项后面不会带档位名,旁边的`档位`按钮可以去直播间接一次(主播没开播时会失败并提示,服务端本身要重试三次、约 5 秒,所以点下去会先给一句`正在读取码率档位…`)。读档位的时机是:录制开始取流时、页面上点`档位`按钮、以及发现主播在直播但还没有档位缓存时自动读一次(同一个主播 10 秒内只读一次)。录制始终是原画质直接保存(`-c copy`),不会重新编码,所以只能在这几个档位里挑。
|
||||||
|
|
||||||
录播原片和弹幕文件(`.ass`、`.nick.ass`、`.jsonl`)保存在保存目录下的`recordSubdir`子文件夹里(默认`record`),两者同名配对才能自动合并;合并后的成片保存在`mergeSubdir`子文件夹里(默认`merged`),留空则用默认值,两个名字不能相同。如果设置了`mergeOutputDir`,成片优先移动到那里。
|
录播原片和弹幕文件(`.ass`、`.nick.ass`、`.jsonl`)保存在保存目录下的`recordSubdir`子文件夹里(默认`record`),**录制过程中 ffmpeg 就直接写在这里**,不会再先落到别处中转;两者同名配对才能自动合并。保存目录的优先级是:`directory` > 命令行`-record`指定的文件夹 > 程序所在目录。合并后的成片保存在`mergeSubdir`子文件夹里(默认`merged`),留空则用默认值,两个名字不能相同。如果设置了`mergeOutputDir`,成片优先移动到那里。
|
||||||
|
|
||||||
合成**先在录播文件夹里做完,写完了才落到成片文件夹**(同一个盘时是直接改名,几乎瞬时;跨盘会自动拷贝):成片文件夹是网络盘、外接盘或者中途断线时,不会在里面留下一个写到一半、看起来却像正常成片的文件。合成失败或中途强杀程序时,临时文件(`acfunlive_chunk_*`、`*.acfunlive.part`)不会出现在文件管理的列表里,程序下次启动会自动清掉。
|
合成**先在录播文件夹里做完,写完了才落到成片文件夹**(同一个盘时是直接改名,几乎瞬时;跨盘会自动拷贝):成片文件夹是网络盘、外接盘或者中途断线时,不会在里面留下一个写到一半、看起来却像正常成片的文件。合成失败或中途强杀程序时,临时文件(`acfunlive_chunk_*`、`*.acfunlive.part`)不会出现在文件管理的列表里,程序下次启动会自动清掉。
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,14 @@ func findMovedFile(path string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 收拢到实际位置:找不着时退回原路径,让后面的报错还能指出「本来想找哪儿」
|
||||||
|
func resolveAfterMove(path string) string {
|
||||||
|
if p := findMovedFile(path); p != "" {
|
||||||
|
return p
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
// 录播因为断流可能分成多段:单段直接使用,多段先拼成完整视频并把弹幕时间映射到拼接后的时间轴。
|
// 录播因为断流可能分成多段:单段直接使用,多段先拼成完整视频并把弹幕时间映射到拼接后的时间轴。
|
||||||
// 返回合并要用的视频、字幕文件和弹幕相对视频起点的时间偏移、**实际参与合并的源文件**,
|
// 返回合并要用的视频、字幕文件和弹幕相对视频起点的时间偏移、**实际参与合并的源文件**,
|
||||||
// 以及清理临时文件的函数。used 是给「合并完删源视频」用的:中途回退成「只合并第一个分段」时,
|
// 以及清理临时文件的函数。used 是给「合并完删源视频」用的:中途回退成「只合并第一个分段」时,
|
||||||
@@ -92,6 +100,10 @@ func findMovedFile(path string) string {
|
|||||||
func prepareForMerge(ffmpegFile string, segs []recordSegment, jsonlFile string, danmuStartNs int64) (string, string, float64, []string, func(), error) {
|
func prepareForMerge(ffmpegFile string, segs []recordSegment, jsonlFile string, danmuStartNs int64) (string, string, float64, []string, func(), error) {
|
||||||
noop := func() {}
|
noop := func() {}
|
||||||
|
|
||||||
|
// 弹幕文件同样会被移动过去。以前只有视频分段走了查找,弹幕还拿着录制时的路径,
|
||||||
|
// 结果就是视频找得到、弹幕找不到,合并以「jsonl 文件不存在」告终
|
||||||
|
jsonlFile = resolveAfterMove(jsonlFile)
|
||||||
|
|
||||||
// 录制结束时文件可能已经被移动到保存目录,逐个找实际位置
|
// 录制结束时文件可能已经被移动到保存目录,逐个找实际位置
|
||||||
files := make([]string, 0, len(segs))
|
files := make([]string, 0, len(segs))
|
||||||
starts := make([]int64, 0, len(segs))
|
starts := make([]int64, 0, len(segs))
|
||||||
@@ -288,6 +300,10 @@ func autoMergeOutFile(jsonlFile string) string {
|
|||||||
func autoMergeJob(uid int, segs []recordSegment, jsonlFile string, danmuStartNs int64) error {
|
func autoMergeJob(uid int, segs []recordSegment, jsonlFile string, danmuStartNs int64) error {
|
||||||
lPrintln("开始自动合并" + longID(uid) + "的录播和弹幕层")
|
lPrintln("开始自动合并" + longID(uid) + "的录播和弹幕层")
|
||||||
|
|
||||||
|
// 录制结束时弹幕文件可能已被移动到保存目录,先把路径收拢到实际位置:
|
||||||
|
// 中间成片写到哪、事后删哪几个弹幕文件都按这一条路径来,用旧路径会指向错误的地方
|
||||||
|
jsonlFile = resolveAfterMove(jsonlFile)
|
||||||
|
|
||||||
// 视频和弹幕是一次录制产生的,文件名去掉扩展名后应该一致(扩展名由 recordFormat 决定)
|
// 视频和弹幕是一次录制产生的,文件名去掉扩展名后应该一致(扩展名由 recordFormat 决定)
|
||||||
if len(segs) > 0 {
|
if len(segs) > 0 {
|
||||||
videoBase := filepath.Base(strings.TrimSuffix(segs[0].file, filepath.Ext(segs[0].file)))
|
videoBase := filepath.Base(strings.TrimSuffix(segs[0].file, filepath.Ext(segs[0].file)))
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 录制结束时文件会被移动到保存目录,视频分段和弹幕文件都要按新位置找。
|
||||||
|
// 以前只有视频分段走了查找,弹幕还拿着录制时的路径:视频找到了、弹幕找不到,
|
||||||
|
// 整场合并以「jsonl 文件不存在」告终(用户看到的就是合成失败 + 一条失败推送)。
|
||||||
|
func TestPrepareForMergeFindsMovedJsonl(t *testing.T) {
|
||||||
|
savedConfig := config
|
||||||
|
defer func() { config = savedConfig }()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
config.Directory = root
|
||||||
|
config.RecordSubdir = "record"
|
||||||
|
if err := os.MkdirAll(recordSaveDir(), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
name := "2026-09-27 22-40-42 甲 标题"
|
||||||
|
// 录制时的位置:文件已经被搬走,这里不该再有
|
||||||
|
oldDir := t.TempDir()
|
||||||
|
oldVideo := filepath.Join(oldDir, name+".mp4")
|
||||||
|
oldJsonl := filepath.Join(oldDir, name+".jsonl")
|
||||||
|
// 实际位置
|
||||||
|
newVideo := filepath.Join(recordSaveDir(), name+".mp4")
|
||||||
|
newJsonl := filepath.Join(recordSaveDir(), name+".jsonl")
|
||||||
|
for _, p := range []string{newVideo, newJsonl} {
|
||||||
|
if err := os.WriteFile(p, []byte("{}\n"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
segs := []recordSegment{{file: oldVideo, startNs: 0}}
|
||||||
|
video, jsonl, _, _, cleanup, err := prepareForMerge("没有这个-ffmpeg.exe", segs, oldJsonl, 0)
|
||||||
|
defer cleanup()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("单段不该报错:%v", err)
|
||||||
|
}
|
||||||
|
if video != newVideo {
|
||||||
|
t.Errorf("视频应当按保存目录里的实际位置找:\n got %s\n want %s", video, newVideo)
|
||||||
|
}
|
||||||
|
if jsonl != newJsonl {
|
||||||
|
t.Errorf("弹幕文件应当按保存目录里的实际位置找:\n got %s\n want %s", jsonl, newJsonl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件确实不在(既没在录制时的位置、也不在保存目录)时,要退回原路径而不是空串:
|
||||||
|
// 否则报错会变成「文件不存在:」,看不出到底缺的是哪个
|
||||||
|
func TestResolveAfterMoveKeepsOriginalWhenMissing(t *testing.T) {
|
||||||
|
savedConfig := config
|
||||||
|
defer func() { config = savedConfig }()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
config.Directory = root
|
||||||
|
config.RecordSubdir = "record"
|
||||||
|
|
||||||
|
gone := filepath.Join(t.TempDir(), "这个目录是对的但文件不在.mp4")
|
||||||
|
if got := resolveAfterMove(gone); got != gone {
|
||||||
|
t.Errorf("找不到时应当退回原路径:got %q want %q", got, gone)
|
||||||
|
}
|
||||||
|
|
||||||
|
real := filepath.Join(root, "record", "在保存目录里.mp4")
|
||||||
|
if err := os.MkdirAll(filepath.Dir(real), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(real, []byte("x"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if got := resolveAfterMove(real); got != real {
|
||||||
|
t.Errorf("文件就在原路径时不该改:got %q want %q", got, real)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -129,7 +129,7 @@ var configMu sync.Mutex
|
|||||||
// 设置数据
|
// 设置数据
|
||||||
type configData struct {
|
type configData struct {
|
||||||
UIDs uidList `json:"uids"` // 要录制的主播 uid 列表和各自的开关
|
UIDs uidList `json:"uids"` // 要录制的主播 uid 列表和各自的开关
|
||||||
Directory string `json:"directory"` // 弹幕文件录制结束后会被移动到该文件夹
|
Directory string `json:"directory"` // 录播和弹幕文件保存的根目录,留空用 -record 指定的文件夹
|
||||||
WebPort int `json:"webPort"` // web UI 的本地端口,为 0 时不启动 web UI
|
WebPort int `json:"webPort"` // web UI 的本地端口,为 0 时不启动 web UI
|
||||||
|
|
||||||
AutoMerge bool `json:"autoMerge"` // 直播结束后自动把弹幕层合并进录播
|
AutoMerge bool `json:"autoMerge"` // 直播结束后自动把弹幕层合并进录播
|
||||||
@@ -190,25 +190,29 @@ func saveRootDir() string {
|
|||||||
if config.Directory != "" {
|
if config.Directory != "" {
|
||||||
return absOrSelf(config.Directory)
|
return absOrSelf(config.Directory)
|
||||||
}
|
}
|
||||||
if recordDir == nil {
|
if recordDir != nil && *recordDir != "" {
|
||||||
return ""
|
return absOrSelf(*recordDir)
|
||||||
}
|
}
|
||||||
return absOrSelf(*recordDir)
|
// 命令行参数还没解析时(比如 initialize 之前被调用)退回程序所在目录,
|
||||||
|
// 再不济退回当前工作目录:这里返回空串的话,后面拼出来的就是个相对路径,
|
||||||
|
// ffmpeg 会按自己的当前目录去写,录制文件就不知道落到哪去了
|
||||||
|
if exeDir != "" {
|
||||||
|
return absOrSelf(exeDir)
|
||||||
|
}
|
||||||
|
if wd, err := os.Getwd(); err == nil {
|
||||||
|
return wd
|
||||||
|
}
|
||||||
|
return "."
|
||||||
}
|
}
|
||||||
|
|
||||||
// 录播和弹幕文件保存的文件夹
|
// 录播和弹幕文件保存的文件夹。
|
||||||
|
// 录制过程中 ffmpeg 就直接写在这里,不会再中途挪窝:以前单独有一个「录制中的目录」,
|
||||||
|
// 它只认 -record 参数不看 directory 设置,配了保存目录的人会发现文件先落在程序目录下,
|
||||||
|
// 录完才搬一次(跨盘就是整份文件再复制一遍),提前中断还会留在那儿。
|
||||||
func recordSaveDir() string {
|
func recordSaveDir() string {
|
||||||
return filepath.Join(saveRootDir(), config.RecordSubdir)
|
return filepath.Join(saveRootDir(), config.RecordSubdir)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 录制过程中文件直接写入的文件夹
|
|
||||||
func recordingDir() string {
|
|
||||||
if recordDir == nil {
|
|
||||||
return config.RecordSubdir
|
|
||||||
}
|
|
||||||
return filepath.Join(absOrSelf(*recordDir), config.RecordSubdir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 合并成片保存的文件夹
|
// 合并成片保存的文件夹
|
||||||
func mergeSaveDir() string {
|
func mergeSaveDir() string {
|
||||||
if config.MergeOutputDir != "" {
|
if config.MergeOutputDir != "" {
|
||||||
@@ -400,6 +404,20 @@ func currentAuthToken() string {
|
|||||||
return config.AuthToken
|
return config.AuthToken
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 检查配置里的文件夹字段,返回错误信息,空字符串表示通过。
|
||||||
|
// 启动读配置、web UI 改设置、转码排队都用这一份 —— 以前各自实现,
|
||||||
|
// 启动那份直接 os.Stat + checkErr,目录不存在时会 panic 崩掉整个进程。
|
||||||
|
func checkDirField(path, name string) string {
|
||||||
|
if path == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
info, err := os.Stat(path)
|
||||||
|
if err != nil || !info.IsDir() {
|
||||||
|
return name + " 必须是存在的文件夹:" + path
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
// 检查 config.json 里的配置
|
// 检查 config.json 里的配置
|
||||||
func checkConfig() {
|
func checkConfig() {
|
||||||
for _, u := range config.UIDs {
|
for _, u := range config.UIDs {
|
||||||
@@ -412,21 +430,15 @@ func checkConfig() {
|
|||||||
lPrintErr(configFile + "里的 webPort 必须为 0(不启动 web UI)或者大于 1023 且少于 65536")
|
lPrintErr(configFile + "里的 webPort 必须为 0(不启动 web UI)或者大于 1023 且少于 65536")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
if config.Directory != "" {
|
// 目录不在(盘没插、路径拼错)要给人话提示然后退出,不能直接 panic:
|
||||||
info, err := os.Stat(config.Directory)
|
// 崩溃时文件系统风险未知,而且服务被托管时会被反复拉起来
|
||||||
checkErr(err)
|
if msg := checkDirField(config.Directory, configFile+"里的directory"); msg != "" {
|
||||||
if !info.IsDir() {
|
lPrintErr(msg)
|
||||||
lPrintErrf("%s里的directory必须是存在的文件夹:%s", configFile, config.Directory)
|
os.Exit(1)
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if config.MergeOutputDir != "" {
|
if msg := checkDirField(config.MergeOutputDir, configFile+"里的mergeOutputDir"); msg != "" {
|
||||||
info, err := os.Stat(config.MergeOutputDir)
|
lPrintErr(msg)
|
||||||
checkErr(err)
|
os.Exit(1)
|
||||||
if !info.IsDir() {
|
|
||||||
lPrintErrf("%s里的mergeOutputDir必须是存在的文件夹:%s", configFile, config.MergeOutputDir)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if config.MergeBitrate < 0 {
|
if config.MergeBitrate < 0 {
|
||||||
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
lPrintErrf("%s里的mergeBitrate不能小于 0", configFile)
|
||||||
@@ -480,7 +492,7 @@ func checkConfig() {
|
|||||||
config.FFmpegPath = ""
|
config.FFmpegPath = ""
|
||||||
}
|
}
|
||||||
// 确保保存文件夹存在
|
// 确保保存文件夹存在
|
||||||
for _, d := range []string{recordingDir(), recordSaveDir(), mergeSaveDir()} {
|
for _, d := range []string{recordSaveDir(), mergeSaveDir()} {
|
||||||
if err := os.MkdirAll(d, 0755); err != nil {
|
if err := os.MkdirAll(d, 0755); err != nil {
|
||||||
lPrintErrf("创建文件夹 %s 失败:%v", d, err)
|
lPrintErrf("创建文件夹 %s 失败:%v", d, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -1119,7 +1131,9 @@ func (s *streamer) moveDanmuFiles(assFile string) {
|
|||||||
s.moveFile(jsonlFile)
|
s.moveFile(jsonlFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 移动文件
|
// 把送出文件到保存目录下,路径一致时什么也不做。
|
||||||
|
// 录制的产出本来就写在 recordSaveDir() 里(见 recordSaveDir 的注释),
|
||||||
|
// 这里是条保险:路径由别处给进来、或者运行中被改过保存目录时,仍以当前配置为准收拢一次。
|
||||||
func (s *streamer) moveFile(oldFile string) {
|
func (s *streamer) moveFile(oldFile string) {
|
||||||
if oldFile == "" {
|
if oldFile == "" {
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -145,3 +146,48 @@ func TestReloadConfigKeepsMergeTempFiles(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 保存目录填错(盘没插、路径拼错)时启动必须给一句人话再退出,
|
||||||
|
// 不能 panic 崩掉整个进程 —— 服务被托管时崩了还会被反复拉起来
|
||||||
|
func TestCheckDirFieldReportsMissingDir(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
if msg := checkDirField("", "directory"); msg != "" {
|
||||||
|
t.Errorf("留空表示不指定,不该报错:%s", msg)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(root, "directory"); msg != "" {
|
||||||
|
t.Errorf("存在的文件夹不该报错:%s", msg)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(filepath.Join(root, "没有这个目录"), "directory"); !strings.Contains(msg, "必须是存在的文件夹") {
|
||||||
|
t.Errorf("目录不存在时要给出提示,实际返回 %q", msg)
|
||||||
|
}
|
||||||
|
file := filepath.Join(root, "afile")
|
||||||
|
if err := os.WriteFile(file, []byte("x"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if msg := checkDirField(file, "directory"); msg == "" {
|
||||||
|
t.Error("指向文件而不是文件夹时应当报错")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkConfig 里曾经直接 os.Stat + checkErr:目录不在时会 panic 崩掉进程。
|
||||||
|
// 这条路径没法在测试里真跑一遍(失败时是 os.Exit(1),会把整个测试进程带走),
|
||||||
|
// 所以退一步把「只能给提示再退出」钉在源码上。
|
||||||
|
func TestCheckConfigNeverPanicsOnMissingDir(t *testing.T) {
|
||||||
|
src, err := os.ReadFile("config.go")
|
||||||
|
if err != nil {
|
||||||
|
t.Skip("读不到 config.go:" + err.Error())
|
||||||
|
}
|
||||||
|
text := string(src)
|
||||||
|
start := strings.Index(text, "func checkConfig()")
|
||||||
|
if start < 0 {
|
||||||
|
t.Fatal("config.go 里找不到 checkConfig")
|
||||||
|
}
|
||||||
|
rest := text[start:]
|
||||||
|
end := strings.Index(rest, "\n}\n")
|
||||||
|
if end < 0 {
|
||||||
|
t.Fatal("checkConfig 的函数体没有正常收尾")
|
||||||
|
}
|
||||||
|
if strings.Contains(rest[:end], "checkErr(") {
|
||||||
|
t.Error("checkConfig 里不能用 checkErr:出错会变成 panic 崩进程,改成 lPrintErr + os.Exit(1)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -24,5 +24,5 @@ func transFilename(filename string) string {
|
|||||||
}
|
}
|
||||||
filename = filename[:cut]
|
filename = filename[:cut]
|
||||||
}
|
}
|
||||||
return filepath.Join(recordingDir(), filename)
|
return filepath.Join(recordSaveDir(), filename)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// 录制过程中 ffmpeg 是按 transFilename 给的路径写文件的。
|
||||||
|
// 以前这条路径走的是 -record 参数而不是 directory 设置,配了保存目录的人会看到
|
||||||
|
// 文件先落在程序目录里、录完才搬一次;这里把「必须等于最终保存目录」钉死。
|
||||||
|
func TestTransFilenameSavesIntoConfiguredDir(t *testing.T) {
|
||||||
|
savedConfig, savedRecordDir := config, recordDir
|
||||||
|
defer func() { config, recordDir = savedConfig, savedRecordDir }()
|
||||||
|
|
||||||
|
root, dashDir := t.TempDir(), t.TempDir()
|
||||||
|
recordDir = &dashDir
|
||||||
|
config.RecordSubdir = "record"
|
||||||
|
|
||||||
|
// 配了 directory:不管 -record 参数是什么,都得以它为准
|
||||||
|
config.Directory = root
|
||||||
|
got := filepath.Clean(transFilename("2026-09-27 22-40-42 甲 标题.mp4"))
|
||||||
|
want := filepath.Join(root, "record", "2026-09-27 22-40-42 甲 标题.mp4")
|
||||||
|
if got != filepath.Clean(want) {
|
||||||
|
t.Errorf("配了 directory 后录制路径不对:\n got %s\n want %s", got, want)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(got, filepath.Clean(root)) {
|
||||||
|
t.Errorf("录制路径落在了保存目录之外:%s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 没配 directory:回落到 -record 指定的文件夹
|
||||||
|
config.Directory = ""
|
||||||
|
got = filepath.Clean(transFilename("x.mp4"))
|
||||||
|
if filepath.Dir(got) != filepath.Clean(filepath.Join(dashDir, "record")) {
|
||||||
|
t.Errorf("directory 为空时应回落到 -record 目录:%s", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 连 -record 都还没解析时也不能拼出相对路径,否则 ffmpeg 会写到进程的工作目录去
|
||||||
|
blank := ""
|
||||||
|
recordDir = &blank
|
||||||
|
if root := saveRootDir(); !filepath.IsAbs(root) {
|
||||||
|
t.Errorf("传入的参数都没解析时 saveRootDir 仍应是绝对路径,实际 %q", root)
|
||||||
|
}
|
||||||
|
if got := filepath.Clean(transFilename("x.mp4")); !filepath.IsAbs(got) {
|
||||||
|
t.Errorf("录制路径不能是相对路径:%q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存目录和录制目录现在是一个,moveFile 再挪一遍等于白复制一份(跨盘时尤其慢)
|
||||||
|
func TestMoveFileKeepsFileInSaveDir(t *testing.T) {
|
||||||
|
savedConfig, savedRecordDir := config, recordDir
|
||||||
|
defer func() { config, recordDir = savedConfig, savedRecordDir }()
|
||||||
|
|
||||||
|
root := t.TempDir()
|
||||||
|
recordDir = &root
|
||||||
|
config.Directory = root
|
||||||
|
config.RecordSubdir = "record"
|
||||||
|
|
||||||
|
if err := os.MkdirAll(recordSaveDir(), 0755); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
p := filepath.Join(recordSaveDir(), "2026-09-27 22-40-42 甲 标题.mp4")
|
||||||
|
if err := os.WriteFile(p, []byte("fake"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := &streamer{UID: 1}
|
||||||
|
s.moveFile(p)
|
||||||
|
if _, err := os.Stat(p); err != nil {
|
||||||
|
t.Errorf("文件已经在保存目录里了,moveFile 不该动它:%v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 别处给进来的路径(比如手填的绝对路径)仍要收拢到保存目录
|
||||||
|
outside := filepath.Join(t.TempDir(), "outside.mp4")
|
||||||
|
if err := os.WriteFile(outside, []byte("fake"), 0644); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
s.moveFile(outside)
|
||||||
|
if _, err := os.Stat(outside); !os.IsNotExist(err) {
|
||||||
|
t.Errorf("保存目录之外的文件应当被移动,原位置却还在")
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(recordSaveDir(), "outside.mp4")); err != nil {
|
||||||
|
t.Errorf("文件没有出现在保存目录里:%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+1
-1
@@ -16,7 +16,7 @@ func transFilename(filename string) string {
|
|||||||
// 转换文件名不允许的特殊字符
|
// 转换文件名不允许的特殊字符
|
||||||
re := regexp.MustCompile(`[<>:"/\\|?*\r\n]`)
|
re := regexp.MustCompile(`[<>:"/\\|?*\r\n]`)
|
||||||
filename = re.ReplaceAllString(filename, "-")
|
filename = re.ReplaceAllString(filename, "-")
|
||||||
dir := recordingDir()
|
dir := recordSaveDir()
|
||||||
outFilename := filepath.Join(dir, filename)
|
outFilename := filepath.Join(dir, filename)
|
||||||
if utf8.RuneCountInString(outFilename) <= maxPathRunes {
|
if utf8.RuneCountInString(outFilename) <= maxPathRunes {
|
||||||
return outFilename
|
return outFilename
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
module github.com/orzogc/acfunlive
|
module github.com/orzogc/acfunlive
|
||||||
|
|
||||||
go 1.26.0
|
go 1.27.3
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/PuerkitoBio/goquery v1.13.0
|
github.com/PuerkitoBio/goquery v1.13.0
|
||||||
|
|||||||
+1
-1
@@ -956,7 +956,7 @@ func transcodeHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
if outDir == "" {
|
if outDir == "" {
|
||||||
outDir = mergeSaveDir()
|
outDir = mergeSaveDir()
|
||||||
} else {
|
} else {
|
||||||
if msg := checkWebDir(outDir, "输出目录"); msg != "" {
|
if msg := checkDirField(outDir, "输出目录"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2431,18 +2431,6 @@ func statusHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
checkErr(err)
|
checkErr(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查配置里的文件夹字段,返回错误信息,空字符串表示通过
|
|
||||||
func checkWebDir(path, name string) string {
|
|
||||||
if path == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
info, err := os.Stat(path)
|
|
||||||
if err != nil || !info.IsDir() {
|
|
||||||
return name + " 必须是存在的文件夹:" + path
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修改设置(保存目录和自动合并相关)
|
// 修改设置(保存目录和自动合并相关)
|
||||||
func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
var req struct {
|
var req struct {
|
||||||
@@ -2481,11 +2469,11 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg := checkWebDir(req.Directory, "directory"); msg != "" {
|
if msg := checkDirField(req.Directory, "directory"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if msg := checkWebDir(req.MergeOutputDir, "mergeOutputDir"); msg != "" {
|
if msg := checkDirField(req.MergeOutputDir, "mergeOutputDir"); msg != "" {
|
||||||
http.Error(w, msg, http.StatusBadRequest)
|
http.Error(w, msg, http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -2594,9 +2582,6 @@ func webConfigHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
config.MeowHTMLHeight = clampMeowHTMLHeight(req.MeowHTMLHeight)
|
config.MeowHTMLHeight = clampMeowHTMLHeight(req.MeowHTMLHeight)
|
||||||
config.MeowImgURL = meowImgURL
|
config.MeowImgURL = meowImgURL
|
||||||
// 修改了子文件夹名后确保新文件夹存在
|
// 修改了子文件夹名后确保新文件夹存在
|
||||||
if err := os.MkdirAll(recordingDir(), 0755); err != nil {
|
|
||||||
lPrintWarnf("创建文件夹 %s 失败:%v", recordingDir(), err)
|
|
||||||
}
|
|
||||||
if err := os.MkdirAll(recordSaveDir(), 0755); err != nil {
|
if err := os.MkdirAll(recordSaveDir(), 0755); err != nil {
|
||||||
lPrintWarnf("创建文件夹 %s 失败:%v", recordSaveDir(), err)
|
lPrintWarnf("创建文件夹 %s 失败:%v", recordSaveDir(), err)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user