init
This commit is contained in:
+555
@@ -0,0 +1,555 @@
|
||||
// web UI 的手动合并:自己挑视频和弹幕合成一个成片
|
||||
// 自动合并只在录制自然结束时触发(手动停止过、或者弹幕和视频对不上号的都不会合并),
|
||||
// 这里补上那条路:选文件 → 检查 → 合成
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// 允许作为合并输入的视频扩展名
|
||||
var mergeVideoExts = map[string]struct{}{
|
||||
".mp4": {}, ".m4v": {}, ".flv": {}, ".ts": {}, ".mkv": {}, ".mov": {}, ".webm": {},
|
||||
}
|
||||
|
||||
// 允许作为合并输入的弹幕扩展名
|
||||
var mergeDanmuExts = map[string]struct{}{".jsonl": {}, ".ass": {}}
|
||||
|
||||
// 手动合并的请求体
|
||||
type mergeRequest struct {
|
||||
Videos []string `json:"videos"` // 视频文件,可多个(断流分段):文件名或绝对/相对路径
|
||||
Danmu string `json:"danmu"` // 弹幕文件:.jsonl 直接用,.ass / .nick.ass 先还原成 jsonl
|
||||
Offset *float64 `json:"offset"` // 手动指定的偏移(秒),不填则按文件名里的录制时间推算
|
||||
OutName string `json:"outName"` // 输出文件名,留空用默认名且自动避开重名
|
||||
}
|
||||
|
||||
// 检查结果里每个视频的信息
|
||||
type mergeVideoInfo struct {
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Duration float64 `json:"duration"`
|
||||
InUse bool `json:"inUse"`
|
||||
}
|
||||
|
||||
// 手动合并的检查结果,页面直接展示
|
||||
type mergeInfo struct {
|
||||
Videos []mergeVideoInfo `json:"videos"`
|
||||
DanmuName string `json:"danmuName"`
|
||||
DanmuCount int `json:"danmuCount"`
|
||||
DanmuFirst float64 `json:"danmuFirst"`
|
||||
DanmuLast float64 `json:"danmuLast"`
|
||||
DanmuHasAvatar bool `json:"danmuHasAvatar"`
|
||||
Source string `json:"source"` // 弹幕数据的来源说明(jsonl 还是从 ass 还原)
|
||||
Offset float64 `json:"offset"`
|
||||
OffsetNote string `json:"offsetNote"`
|
||||
OutName string `json:"outName"`
|
||||
Duration float64 `json:"duration"` // 视频总时长(秒)
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// 检查通过后解析好的合并方案,避免检查一次、合成时又重新解析一遍
|
||||
type mergePlan struct {
|
||||
videos []string // 视频的绝对路径,已按录制时间排好序
|
||||
danmu string // 弹幕文件的绝对路径
|
||||
out string // 成片的绝对路径
|
||||
meta danmuMeta
|
||||
offset *float64
|
||||
info mergeInfo
|
||||
}
|
||||
|
||||
// 手动合并的运行状态,页面轮询它来决定按钮是否可用
|
||||
var mergeRun struct {
|
||||
sync.Mutex
|
||||
busy bool
|
||||
outName string
|
||||
lastErr string
|
||||
doneAt int64
|
||||
}
|
||||
|
||||
// 手动合并是否正在进行
|
||||
func mergeRunSnapshot() (busy bool, out string, lastErr string, doneAt int64) {
|
||||
mergeRun.Lock()
|
||||
defer mergeRun.Unlock()
|
||||
return mergeRun.busy, mergeRun.outName, mergeRun.lastErr, mergeRun.doneAt
|
||||
}
|
||||
|
||||
// 解析用户填的路径:可以先当文件名在录播/成片文件夹里找,也可以填绝对或相对路径
|
||||
func resolveMergeInput(value string) (string, error) {
|
||||
value = strings.TrimSpace(value)
|
||||
// 去掉从资源管理器复制路径时带上的引号
|
||||
value = strings.Trim(value, `"'`)
|
||||
if value == "" {
|
||||
return "", errors.New("文件路径为空")
|
||||
}
|
||||
if value == "." || value == ".." || strings.ContainsAny(value, "\x00\r\n") {
|
||||
return "", errors.New("文件路径不合法")
|
||||
}
|
||||
|
||||
var cands []string
|
||||
if filepath.IsAbs(value) {
|
||||
cands = append(cands, value)
|
||||
} else {
|
||||
// 不是绝对路径时优先当成录播目录里的文件名,这样页面下拉里选出来的名字能直接填
|
||||
cands = append(cands, filepath.Join(recordSaveDir(), value))
|
||||
if mergeDir := mergeSaveDir(); normalizePath(mergeDir) != normalizePath(recordSaveDir()) {
|
||||
cands = append(cands, filepath.Join(mergeDir, value))
|
||||
}
|
||||
cands = append(cands, value) // 相对当前工作目录
|
||||
}
|
||||
for _, c := range cands {
|
||||
if info, err := os.Stat(c); err == nil && !info.IsDir() {
|
||||
full, err := filepath.Abs(c)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
return full, nil
|
||||
}
|
||||
}
|
||||
return "", fmt.Errorf("找不到文件:%s", value)
|
||||
}
|
||||
|
||||
// 扩展名白名单的说明文字
|
||||
func extListText(m map[string]struct{}) string {
|
||||
out := make([]string, 0, len(m))
|
||||
for k := range m {
|
||||
out = append(out, k)
|
||||
}
|
||||
sort.Strings(out)
|
||||
return strings.Join(out, " ")
|
||||
}
|
||||
|
||||
// 从文件名前缀推出录制开始的时刻
|
||||
// 本程序(以及上游原版)的文件名都是「2006-01-02 15-04-05 主播名 标题」
|
||||
func nameStartNs(path string) int64 {
|
||||
name := filepath.Base(path)
|
||||
if len(name) < 19 {
|
||||
return 0
|
||||
}
|
||||
t, err := time.ParseInLocation("2006-01-02 15-04-05", name[:19], time.Local)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return t.UnixNano()
|
||||
}
|
||||
|
||||
// 文件名里没有时间戳时,用「修改时间 - 时长」估算视频的起点
|
||||
func estimateStartNs(ffmpegFile, path string) int64 {
|
||||
d := probeDuration(ffmpegFile, path)
|
||||
if d <= 0 {
|
||||
return 0
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return info.ModTime().UnixNano() - int64(d*1e9)
|
||||
}
|
||||
|
||||
// 推测弹幕相对视频起点的时间偏移:视频时间 = 弹幕时间 + 偏移
|
||||
func guessOffset(videos []string, danmuPath string, meta danmuMeta) (float64, string) {
|
||||
if len(videos) == 1 {
|
||||
videoBase := strings.TrimSuffix(filepath.Base(videos[0]), filepath.Ext(videos[0]))
|
||||
if strings.EqualFold(danmuBaseName(danmuPath), videoBase) {
|
||||
return 0, "视频和弹幕文件名一致,按同一场录制处理"
|
||||
}
|
||||
}
|
||||
|
||||
danmuStart, fromMeta := nameStartNs(danmuPath), false
|
||||
if danmuStart == 0 && meta.StartTime > 0 {
|
||||
danmuStart, fromMeta = meta.StartTime, true
|
||||
}
|
||||
if danmuStart == 0 {
|
||||
return 0, "弹幕文件里没有录制开始时间,偏移按 0 处理"
|
||||
}
|
||||
|
||||
videoStart := int64(0)
|
||||
if len(videos) > 0 {
|
||||
videoStart = nameStartNs(videos[0])
|
||||
if videoStart == 0 {
|
||||
videoStart = estimateStartNs(getFFmpeg(), videos[0])
|
||||
}
|
||||
}
|
||||
if videoStart == 0 {
|
||||
return 0, "无法确定视频的开始时间,偏移按 0 处理"
|
||||
}
|
||||
|
||||
off := float64(danmuStart-videoStart) / 1e9
|
||||
note := "按文件名里的录制时间推算"
|
||||
if fromMeta {
|
||||
note = "视频按文件名时间、弹幕按 jsonl 里记录的开始时间推算"
|
||||
}
|
||||
if math.Abs(off) > 60 {
|
||||
note += ";偏移偏大,请确认选的是同一场录制"
|
||||
}
|
||||
return off, note
|
||||
}
|
||||
|
||||
// 准备弹幕源:.jsonl 直接用;.ass / .nick.ass 还原成 jsonl
|
||||
// write 为 false 时只解析不落盘(页面上的「检查」用)
|
||||
type danmuSource struct {
|
||||
jsonlPath string
|
||||
meta danmuMeta
|
||||
count int
|
||||
first float64
|
||||
last float64
|
||||
hasAvatar bool
|
||||
source string // 来源说明
|
||||
}
|
||||
|
||||
func prepareDanmuSource(path string, write bool) (danmuSource, error) {
|
||||
if strings.EqualFold(filepath.Ext(path), ".jsonl") {
|
||||
sum, err := scanJsonlSummary(path)
|
||||
if err != nil {
|
||||
return danmuSource{}, err
|
||||
}
|
||||
return danmuSource{jsonlPath: path, meta: sum.meta, count: sum.count,
|
||||
first: sum.first, last: sum.last, hasAvatar: sum.hasAvatar,
|
||||
source: "jsonl 弹幕数据"}, nil
|
||||
}
|
||||
|
||||
// 旁边已经有同名的 jsonl(本程序录制的就有),它带着头像链接,比从 .ass 还原更好
|
||||
jsonlPath := assSiblingJsonl(path)
|
||||
if info, err := os.Stat(jsonlPath); err == nil && info.Size() > 0 {
|
||||
if sum, err := scanJsonlSummary(jsonlPath); err == nil && sum.count > 0 {
|
||||
lPrintf("发现同名的弹幕数据文件 %s,直接用(比 .ass 多了头像链接)", filepath.Base(jsonlPath))
|
||||
return danmuSource{jsonlPath: jsonlPath, meta: sum.meta, count: sum.count,
|
||||
first: sum.first, last: sum.last, hasAvatar: sum.hasAvatar,
|
||||
source: "同名的 jsonl 弹幕数据"}, nil
|
||||
}
|
||||
}
|
||||
|
||||
meta, records, err := parseAssFile(path)
|
||||
if err != nil {
|
||||
return danmuSource{}, err
|
||||
}
|
||||
src := danmuSource{jsonlPath: jsonlPath, meta: meta, count: len(records),
|
||||
first: records[0].Time, last: records[len(records)-1].Time,
|
||||
source: "由 " + filepath.Base(path) + " 还原"}
|
||||
if !write {
|
||||
return src, nil
|
||||
}
|
||||
|
||||
if err := writeJsonlFile(jsonlPath, meta, records); err != nil {
|
||||
// 目录只读等情况退到临时目录,不影响这次合并
|
||||
tmp := filepath.Join(os.TempDir(), chunkTempPrefix+"converted.jsonl")
|
||||
if err2 := writeJsonlFile(tmp, meta, records); err2 != nil {
|
||||
return src, fmt.Errorf("还原弹幕数据失败:%v", err)
|
||||
}
|
||||
lPrintWarnf("无法把还原结果写入 %s(%v),改用临时文件 %s", jsonlPath, err, tmp)
|
||||
src.jsonlPath = tmp
|
||||
return src, nil
|
||||
}
|
||||
lPrintf("已把 %s 还原成 %s", filepath.Base(path), filepath.Base(jsonlPath))
|
||||
return src, nil
|
||||
}
|
||||
|
||||
// 输出文件名只能是文件名本身,且固定为 mp4
|
||||
func sanitizeOutName(name string) (string, error) {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
return "", errors.New("输出文件名不能为空")
|
||||
}
|
||||
if name == "." || name == ".." || strings.ContainsAny(name, "\x00\r\n") ||
|
||||
name != filepath.Base(filepath.FromSlash(name)) || strings.ContainsAny(name, `/\`) {
|
||||
return "", errors.New("输出文件名不合法,只能填文件名本身")
|
||||
}
|
||||
if !strings.EqualFold(filepath.Ext(name), ".mp4") {
|
||||
name += ".mp4"
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
// 找一个不冲突的输出路径,不默默覆盖上一次的成片
|
||||
func freeOutPath(dir, base string) string {
|
||||
cand := filepath.Join(dir, base+".mp4")
|
||||
if _, err := os.Stat(cand); err != nil {
|
||||
return cand
|
||||
}
|
||||
for i := 2; i < 1000; i++ {
|
||||
p := filepath.Join(dir, fmt.Sprintf("%s(%d).mp4", base, i))
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return p
|
||||
}
|
||||
}
|
||||
return cand
|
||||
}
|
||||
|
||||
// 解析请求、检查文件并算出合并参数
|
||||
func buildMergePlan(req mergeRequest) (mergePlan, error) {
|
||||
var plan mergePlan
|
||||
|
||||
// 视频:一行一个,允许夹带空行
|
||||
var values []string
|
||||
for _, v := range req.Videos {
|
||||
for _, p := range strings.FieldsFunc(v, func(r rune) bool { return r == '\n' || r == '\r' }) {
|
||||
if t := strings.TrimSpace(p); t != "" {
|
||||
values = append(values, t)
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(values) == 0 {
|
||||
return plan, errors.New("请先选择要合并的视频文件")
|
||||
}
|
||||
for _, v := range values {
|
||||
full, err := resolveMergeInput(v)
|
||||
if err != nil {
|
||||
return plan, err
|
||||
}
|
||||
if _, ok := mergeVideoExts[strings.ToLower(filepath.Ext(full))]; !ok {
|
||||
return plan, fmt.Errorf("不支持的视频格式 %s,支持 %s", filepath.Ext(full), extListText(mergeVideoExts))
|
||||
}
|
||||
if isFileActive(full) {
|
||||
return plan, fmt.Errorf("%s 正在录制或写入中,等它结束再合并", filepath.Base(full))
|
||||
}
|
||||
plan.videos = append(plan.videos, full)
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Danmu) == "" {
|
||||
return plan, errors.New("请先选择弹幕文件")
|
||||
}
|
||||
danmuPath, err := resolveMergeInput(req.Danmu)
|
||||
if err != nil {
|
||||
return plan, err
|
||||
}
|
||||
if _, ok := mergeDanmuExts[strings.ToLower(filepath.Ext(danmuPath))]; !ok {
|
||||
return plan, fmt.Errorf("不支持的弹幕格式 %s,支持 %s", filepath.Ext(danmuPath), extListText(mergeDanmuExts))
|
||||
}
|
||||
if isFileActive(danmuPath) {
|
||||
return plan, fmt.Errorf("%s 正在录制或写入中,等它结束再合并", filepath.Base(danmuPath))
|
||||
}
|
||||
plan.danmu = danmuPath
|
||||
|
||||
// 先只解析不落盘,检查阶段不应该产生文件
|
||||
src, err := prepareDanmuSource(danmuPath, false)
|
||||
if err != nil {
|
||||
return plan, err
|
||||
}
|
||||
if src.count == 0 {
|
||||
return plan, errNoDanmu
|
||||
}
|
||||
plan.meta = src.meta
|
||||
|
||||
// 按录制的开始时间排序,用户填反了也能自动纠正
|
||||
sort.SliceStable(plan.videos, func(i, j int) bool {
|
||||
si, sj := nameStartNs(plan.videos[i]), nameStartNs(plan.videos[j])
|
||||
if si == 0 || sj == 0 {
|
||||
return plan.videos[i] < plan.videos[j]
|
||||
}
|
||||
return si < sj
|
||||
})
|
||||
|
||||
info := mergeInfo{
|
||||
DanmuName: filepath.Base(danmuPath),
|
||||
DanmuCount: src.count,
|
||||
DanmuFirst: src.first,
|
||||
DanmuLast: src.last,
|
||||
DanmuHasAvatar: src.hasAvatar,
|
||||
Source: src.source,
|
||||
}
|
||||
ffmpegFile := getFFmpeg()
|
||||
for _, f := range plan.videos {
|
||||
fi, err := os.Stat(f)
|
||||
if err != nil {
|
||||
return plan, fmt.Errorf("读不到文件信息:%s", filepath.Base(f))
|
||||
}
|
||||
d := 0.0
|
||||
if ffmpegFile != "" {
|
||||
d = probeDuration(ffmpegFile, f)
|
||||
}
|
||||
info.Duration += d
|
||||
info.Videos = append(info.Videos, mergeVideoInfo{
|
||||
Name: filepath.Base(f), Size: fi.Size(), Duration: d, InUse: isFileActive(f),
|
||||
})
|
||||
}
|
||||
if ffmpegFile == "" {
|
||||
return plan, errors.New("找不到 ffmpeg,请先在设置里填写 ffmpeg 路径")
|
||||
}
|
||||
|
||||
info.Offset, info.OffsetNote = guessOffset(plan.videos, danmuPath, src.meta)
|
||||
if req.Offset != nil {
|
||||
plan.offset = req.Offset
|
||||
info.Offset = *req.Offset
|
||||
info.OffsetNote = "按你填写的偏移"
|
||||
}
|
||||
|
||||
mergeDir := mergeSaveDir()
|
||||
if strings.TrimSpace(req.OutName) == "" {
|
||||
plan.out = freeOutPath(mergeDir, danmuBaseName(danmuPath)+"_弹幕合并")
|
||||
} else {
|
||||
name, err := sanitizeOutName(req.OutName)
|
||||
if err != nil {
|
||||
return plan, err
|
||||
}
|
||||
plan.out = filepath.Join(mergeDir, name)
|
||||
}
|
||||
info.OutName = filepath.Base(plan.out)
|
||||
|
||||
if !src.hasAvatar {
|
||||
info.Warnings = append(info.Warnings,
|
||||
"这个弹幕文件里没有头像链接,合成时会按 uid 向 AcFun 反查头像;个别查不到的只烧文字")
|
||||
}
|
||||
if info.Duration > 0 && src.last+info.Offset >= info.Duration {
|
||||
info.Warnings = append(info.Warnings, fmt.Sprintf(
|
||||
"最后一条弹幕在 %.1f 秒,加上偏移后已超出视频总长 %.1f 秒,超出的弹幕会被丢掉;"+
|
||||
"如果这个视频只是整场的一段,请把同场的其它分段一起加进来",
|
||||
src.last+info.Offset, info.Duration))
|
||||
}
|
||||
if info.Duration == 0 {
|
||||
info.Warnings = append(info.Warnings, "读不出视频时长,合成时无法裁掉超出视频的弹幕")
|
||||
}
|
||||
|
||||
plan.info = info
|
||||
return plan, nil
|
||||
}
|
||||
|
||||
// 真正执行合并,进度直接打到运行日志里
|
||||
func runManualMerge(plan mergePlan) error {
|
||||
ffmpegFile := getFFmpeg()
|
||||
if ffmpegFile == "" {
|
||||
return errors.New("找不到 ffmpeg,请先在设置里填写 ffmpeg 路径")
|
||||
}
|
||||
|
||||
src, err := prepareDanmuSource(plan.danmu, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 每一段的录制开始时刻取自文件名,用来算偏移和拼接后的时间轴
|
||||
segs := make([]recordSegment, 0, len(plan.videos))
|
||||
for _, f := range plan.videos {
|
||||
start := nameStartNs(f)
|
||||
if start == 0 {
|
||||
start = estimateStartNs(ffmpegFile, f)
|
||||
}
|
||||
if start == 0 {
|
||||
start = time.Now().UnixNano()
|
||||
}
|
||||
segs = append(segs, recordSegment{file: f, startNs: start})
|
||||
}
|
||||
if len(segs) > 1 {
|
||||
lPrintf("手动合并:%d 个分段按录制时间顺序拼成一个视频后再合成", len(segs))
|
||||
}
|
||||
|
||||
danmuStartNs := nameStartNs(plan.danmu)
|
||||
if danmuStartNs == 0 {
|
||||
danmuStartNs = src.meta.StartTime
|
||||
}
|
||||
if danmuStartNs == 0 {
|
||||
danmuStartNs = segs[0].startNs
|
||||
}
|
||||
|
||||
// 复用自动合并那条链路:多段会先拼接并把弹幕时间重映射到拼接后的时间轴
|
||||
video, jsonl, autoOffset, cleanup, err := prepareForMerge(ffmpegFile, segs, src.jsonlPath, danmuStartNs)
|
||||
defer cleanup()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
offset, note := autoOffset, "按文件名里的录制时间自动推算"
|
||||
if plan.offset != nil {
|
||||
offset, note = *plan.offset, "按你填写的偏移"
|
||||
}
|
||||
if math.Abs(offset) > 60 {
|
||||
lPrintWarnf("偏移 %.1f 秒偏大,如果弹幕整体对不上画面,调整偏移后重新合并", offset)
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(plan.out), 0755); err != nil {
|
||||
return fmt.Errorf("创建成片文件夹失败:%v", err)
|
||||
}
|
||||
if _, err := os.Stat(plan.out); err == nil {
|
||||
lPrintWarn("成片已存在,将被覆盖:" + plan.out)
|
||||
}
|
||||
|
||||
lPrintf("手动合并开始:视频 %s,弹幕 %s,偏移 %.3f 秒(%s)",
|
||||
filepath.Base(video), filepath.Base(jsonl), offset, note)
|
||||
|
||||
// 标记成正在写入,避免在文件管理里把还没写完的成片删掉
|
||||
markFileActive(plan.out, true)
|
||||
burnErr := burnDanmu(jsonl, video, plan.out, offset)
|
||||
markFileActive(plan.out, false)
|
||||
if burnErr != nil {
|
||||
return burnErr
|
||||
}
|
||||
|
||||
lPrintln("手动合并完成:" + plan.out)
|
||||
return nil
|
||||
}
|
||||
|
||||
// 检查合并参数,回显时长、弹幕条数和推测的偏移,不做实际合成
|
||||
func mergeInfoHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req mergeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
plan, err := buildMergePlan(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if err := json.NewEncoder(w).Encode(plan.info); err != nil {
|
||||
lPrintErrf("输出合并检查结果失败:%v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// 开始手动合并:立刻返回,合成在后台进行
|
||||
func mergeHandler(w http.ResponseWriter, r *http.Request) {
|
||||
var req mergeRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
http.Error(w, "请求不是合法的 json:"+err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
plan, err := buildMergePlan(req)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
mergeRun.Lock()
|
||||
if mergeRun.busy {
|
||||
busyOut := mergeRun.outName
|
||||
mergeRun.Unlock()
|
||||
http.Error(w, "已经有一个手动合并在进行("+busyOut+"),等它结束再说", http.StatusConflict)
|
||||
return
|
||||
}
|
||||
mergeRun.busy = true
|
||||
mergeRun.outName = plan.info.OutName
|
||||
mergeRun.lastErr = ""
|
||||
mergeRun.Unlock()
|
||||
|
||||
safeGo("手动合并", func() {
|
||||
defer func() {
|
||||
mergeRun.Lock()
|
||||
mergeRun.busy = false
|
||||
mergeRun.doneAt = time.Now().Unix()
|
||||
mergeRun.Unlock()
|
||||
}()
|
||||
if err := runManualMerge(plan); err != nil {
|
||||
msg := err.Error()
|
||||
if err == errNoDanmu {
|
||||
msg = "弹幕文件里没有弹幕"
|
||||
lPrintWarn("弹幕文件里没有弹幕,取消手动合并")
|
||||
} else {
|
||||
lPrintErrf("手动合并失败:%v", err)
|
||||
}
|
||||
mergeRun.Lock()
|
||||
mergeRun.lastErr = msg
|
||||
mergeRun.Unlock()
|
||||
}
|
||||
})
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"outName":` + strconv.Quote(plan.info.OutName) + `}`))
|
||||
}
|
||||
Reference in New Issue
Block a user