2026-01-28 10:48:14 +00:00
|
|
|
package video
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
2026-01-30 14:27:44 +00:00
|
|
|
"log"
|
2026-01-28 10:48:14 +00:00
|
|
|
"os"
|
|
|
|
|
"path"
|
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
|
|
"arimelody.space/live-vod-uploader/youtube"
|
|
|
|
|
ffmpeg "github.com/u2takey/ffmpeg-go"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
type (
|
|
|
|
|
probeFormat struct {
|
|
|
|
|
Duration string `json:"duration"`
|
|
|
|
|
}
|
|
|
|
|
probeData struct {
|
|
|
|
|
Format probeFormat `json:"format"`
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
|
2026-01-30 14:14:54 +00:00
|
|
|
func ConcatVideo(video *youtube.Video, vodFiles []string, verbose bool) (int64, error) {
|
2026-01-28 10:48:14 +00:00
|
|
|
fileListPath := path.Join(
|
|
|
|
|
path.Dir(video.Filename),
|
|
|
|
|
"files.txt",
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
totalDuration := float64(0.0)
|
|
|
|
|
fileListString := ""
|
|
|
|
|
for _, file := range vodFiles {
|
|
|
|
|
fileListString += fmt.Sprintf("file '%s'\n", file)
|
|
|
|
|
jsonProbe, err := ffmpeg.Probe(path.Join(path.Dir(video.Filename), file))
|
|
|
|
|
if err != nil {
|
2026-01-30 14:14:54 +00:00
|
|
|
return 0, fmt.Errorf("failed to probe file `%s`: %v", file, err)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
probe := probeData{}
|
|
|
|
|
json.Unmarshal([]byte(jsonProbe), &probe)
|
|
|
|
|
duration, err := strconv.ParseFloat(probe.Format.Duration, 64)
|
|
|
|
|
if err != nil {
|
2026-01-30 14:14:54 +00:00
|
|
|
return 0, fmt.Errorf("failed to parse duration of file `%s`: %v", file, err)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
totalDuration += duration
|
|
|
|
|
}
|
|
|
|
|
err := os.WriteFile(
|
|
|
|
|
fileListPath,
|
|
|
|
|
[]byte(fileListString),
|
2026-01-28 12:50:11 +00:00
|
|
|
0644,
|
2026-01-28 10:48:14 +00:00
|
|
|
)
|
|
|
|
|
if err != nil {
|
2026-01-30 14:14:54 +00:00
|
|
|
return 0, fmt.Errorf("failed to write file list: %v", err)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-28 12:50:11 +00:00
|
|
|
stream := ffmpeg.Input(fileListPath, ffmpeg.KwArgs{
|
2026-01-28 10:48:14 +00:00
|
|
|
"f": "concat",
|
|
|
|
|
"safe": "0",
|
|
|
|
|
}).Output(video.Filename, ffmpeg.KwArgs{
|
|
|
|
|
"c": "copy",
|
2026-01-28 12:50:11 +00:00
|
|
|
}).OverWriteOutput()
|
|
|
|
|
if verbose { stream = stream.ErrorToStdOut() }
|
|
|
|
|
err = stream.Run()
|
2026-01-28 10:48:14 +00:00
|
|
|
|
|
|
|
|
if err != nil {
|
2026-01-30 14:14:54 +00:00
|
|
|
return 0, fmt.Errorf("ffmpeg error: %v", err)
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:27:44 +00:00
|
|
|
if err = os.Remove(fileListPath); err != nil {
|
|
|
|
|
log.Fatalf("Failed to remove temporary file list: %v", err)
|
|
|
|
|
// not the end of the world; move along
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-30 14:14:54 +00:00
|
|
|
fileInfo, err := os.Stat(video.Filename)
|
|
|
|
|
if err != nil { return 0, fmt.Errorf("failed to read output file: %v", err) }
|
|
|
|
|
|
|
|
|
|
return fileInfo.Size(), nil
|
2026-01-28 10:48:14 +00:00
|
|
|
}
|