Files
av1-go/main.go

390 lines
10 KiB
Go

package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
)
var (
RequiredTools = []string{
"ffmpeg", "ffprobe", "mkvmerge",
"opusenc", "mediainfo", "av1an", "HandBrakeCLI", "ffmsindex",
}
DirCompleted = "completed"
DirOriginal = "original"
DirConvLogs = "conv_logs"
RemuxCodecs = map[string]bool{"aac": true, "opus": true}
)
func main() {
noDownmix := flag.Bool("no-downmix", false, "Preserve original audio channel layout")
autocrop := flag.Bool("autocrop", false, "Automatically detect and crop black bars")
speed := flag.String("speed", "", "Set encoding speed (slower, slow, medium, fast, faster)")
quality := flag.String("quality", "", "Set encoding quality (lowest, low, medium, high, higher)")
grain := flag.Int("grain", -1, "Set film-grain value")
flag.Parse()
CheckTools(RequiredTools)
if *speed != "" {
SvtAv1Params["speed"] = *speed
}
if *quality != "" {
SvtAv1Params["quality"] = *quality
}
if *grain != -1 {
SvtAv1Params["film-grain"] = *grain
}
os.MkdirAll(DirCompleted, 0755)
os.MkdirAll(DirOriginal, 0755)
os.MkdirAll(DirConvLogs, 0755)
for {
files, err := filepath.Glob("*.mkv")
if err != nil {
fmt.Println("Error listing files:", err)
return
}
// Filter files
var toProcess []string
for _, f := range files {
if strings.HasSuffix(f, ".ut.mkv") || strings.HasPrefix(f, "temp-") || strings.HasPrefix(f, "output-") || strings.HasSuffix(f, ".cfr_temp.mkv") {
continue
}
toProcess = append(toProcess, f)
}
sort.Strings(toProcess)
if len(toProcess) == 0 {
fmt.Println("No more .mkv files found to process. Exiting.")
break
}
file := toProcess[0]
if !IsFfmpegDecodable(file) {
fmt.Printf("ERROR: ffmpeg cannot decode '%s'. Skipping.\n", file)
os.Rename(file, filepath.Join(DirOriginal, filepath.Base(file)))
continue
}
ProcessFile(file, *noDownmix, *autocrop)
}
}
func ProcessFile(filePath string, noDownmix, autocrop bool) {
baseName := strings.TrimSuffix(filepath.Base(filePath), filepath.Ext(filePath))
logFileName := baseName + ".log"
logFilePath := filepath.Join(DirConvLogs, logFileName)
SetupLogging(logFilePath)
LogInfo("STARTING LOG FOR: %s", filepath.Base(filePath))
LogInfo("Processing started at: %s", time.Now().Format(time.RFC3339))
absPath, _ := filepath.Abs(filePath)
LogInfo("Full input file path: %s", absPath)
LogInfo(strings.Repeat("-", 80))
intermediateOutput := "output-" + filepath.Base(filePath)
var audioTempDir string
var handbrakeCleanup string
processingError := false
startTime := time.Now()
defer func() {
LogInfo("--- Starting Universal Cleanup ---")
if audioTempDir != "" {
os.RemoveAll(audioTempDir)
LogInfo(" - Deleted audio temp dir: %s", audioTempDir)
}
if _, err := os.Stat(intermediateOutput); err == nil {
if processingError {
LogInfo(" - WARNING: Error occurred. Intermediate file preserved: %s", intermediateOutput)
} else {
os.Remove(intermediateOutput)
LogInfo(" - Deleted intermediate output file: %s", intermediateOutput)
}
} else {
if !processingError {
LogInfo(" - Intermediate output file successfully moved.")
}
}
LogInfo("\nTotal runtime: %s", time.Since(startTime))
LogInfo("FINISHED LOG FOR: %s", filepath.Base(filePath))
if LogFileHandle != nil {
LogFileHandle.Close()
LogFileHandle = nil
}
}()
// Create audio temp dir
var err error
audioTempDir, err = ioutil.TempDir("", "anime_audio_")
if err != nil {
LogInfo("Error creating temp dir: %v", err)
processingError = true
return
}
LogInfo("Audio temporary directory created at: %s", audioTempDir)
// Analyze file
LogInfo("Analyzing file: %s", absPath)
ffprobeJson, err := RunCmd("ffprobe", []string{"-v", "quiet", "-print_format", "json", "-show_streams", "-show_format", absPath}, true)
if err != nil {
LogInfo("ffprobe failed: %v", err)
processingError = true
return
}
var ffprobeInfo FFprobeOutput
json.Unmarshal([]byte(ffprobeJson), &ffprobeInfo)
mkvmergeJson, err := RunCmd("mkvmerge", []string{"-J", absPath}, true)
if err != nil {
LogInfo("mkvmerge failed: %v", err)
processingError = true
return
}
var mkvInfo MkvMergeOutput
json.Unmarshal([]byte(mkvmergeJson), &mkvInfo)
mediainfoJson, err := RunCmd("mediainfo", []string{"--Output=JSON", "-f", absPath}, true)
if err != nil {
LogInfo("mediainfo failed: %v", err)
processingError = true
return
}
var mediaInfo MediaInfoOutput
json.Unmarshal([]byte(mediainfoJson), &mediaInfo)
// VFR Detection
isVfr := false
targetCfrFps := ""
// Find video track in mediainfo
var videoTrack *MediaInfoTrack
for _, t := range mediaInfo.Media.Track {
if t.Type == "Video" {
// Make a copy to avoid pointer to loop variable issues if we were storing it,
// but here we just assign to a pointer variable.
val := t
videoTrack = &val
break
}
}
if videoTrack != nil {
mode := strings.ToUpper(videoTrack.FrameRateMode)
if mode == "VFR" || mode == "VARIABLE" {
isVfr = true
LogInfo(" - Detected VFR based on MediaInfo FrameRate_Mode: %s", videoTrack.FrameRateMode)
origStr := videoTrack.FrameRateOriginalString
re := regexp.MustCompile(`\((\d+/\d+)\)`)
match := re.FindStringSubmatch(origStr)
if len(match) > 1 {
targetCfrFps = match[1]
} else {
targetCfrFps = videoTrack.FrameRateOriginal
}
if targetCfrFps == "" {
targetCfrFps = videoTrack.FrameRate
if targetCfrFps != "" {
LogInfo(" - Using MediaInfo FrameRate (%s) as fallback.", targetCfrFps)
}
}
if targetCfrFps != "" {
LogInfo(" - Target CFR for HandBrake: %s", targetCfrFps)
if strings.Contains(targetCfrFps, "/") {
parts := strings.Split(targetCfrFps, "/")
if len(parts) == 2 {
num, _ := strconv.ParseFloat(parts[0], 64)
den, _ := strconv.ParseFloat(parts[1], 64)
if den != 0 {
targetCfrFps = fmt.Sprintf("%.3f", num/den)
LogInfo(" - Converted fractional FPS to decimal: %s", targetCfrFps)
}
}
}
} else {
LogInfo(" - Warning: VFR detected but no target CFR found.")
isVfr = false
}
} else {
LogInfo(" - Video appears to be CFR.")
}
}
autocropFilter := ""
if autocrop {
LogInfo("--- Running autocrop detection ---")
autocropFilter = DetectAutocropFilter(absPath)
if autocropFilter != "" {
LogInfo(" - Autocrop filter detected: %s", autocropFilter)
} else {
LogInfo(" - No crop needed or detected.")
}
}
encodedVideoFile, hbCleanup, err := ConvertVideo(baseName, absPath, isVfr, targetCfrFps, autocropFilter)
if err != nil {
LogInfo("Video conversion failed: %v", err)
processingError = true
return
}
handbrakeCleanup = hbCleanup
LogInfo("--- Starting Audio Processing ---")
var processedAudio []ProcessedAudioFile
var remuxAudioTracks []string
// Map mkvmerge tracks
mkvAudioTracks := make(map[int]MkvMergeTrack)
for _, t := range mkvInfo.Tracks {
if t.Type == "audio" {
mkvAudioTracks[t.ID] = t
}
}
// Map mediainfo tracks by StreamOrder
mediaAudioTracks := make(map[int]MediaInfoTrack)
for _, t := range mediaInfo.Media.Track {
if t.Type == "Audio" {
so, _ := strconv.Atoi(t.StreamOrder)
mediaAudioTracks[so] = t
}
}
for _, s := range ffprobeInfo.Streams {
if s.CodecType != "audio" {
continue
}
// Find mkv track
var mkvTrack MkvMergeTrack
found := false
for _, t := range mkvInfo.Tracks {
if t.Type == "audio" && t.Properties.StreamID == s.Index {
mkvTrack = t
found = true
break
}
}
if !found {
// Fallback by index if possible, but stream index might not match track index directly
// Simplified: just skip or warn? Python script had fallback.
}
trackID := mkvTrack.ID
trackTitle := mkvTrack.Properties.TrackName
lang := s.Tags["language"]
if lang == "" {
lang = "und"
}
// Delay
delay := 0
if mt, ok := mediaAudioTracks[s.Index]; ok {
if mt.VideoDelay != "" {
dVal, _ := strconv.ParseFloat(mt.VideoDelay, 64)
// MediaInfo usually gives ms if int, or s if float?
// Python script logic: if < 1, *1000.
if dVal < 1 && dVal > -1 && dVal != 0 {
delay = int(dVal * 1000)
} else {
delay = int(dVal)
}
}
}
LogInfo("Processing Audio Stream #%d (TID: %d, Codec: %s, Channels: %d)", s.Index, trackID, s.CodecName, s.Channels)
if RemuxCodecs[s.CodecName] {
remuxAudioTracks = append(remuxAudioTracks, strconv.Itoa(trackID))
} else {
opusFile, err := ConvertAudioTrack(s.Index, s.Channels, lang, audioTempDir, absPath, !noDownmix)
if err != nil {
LogInfo("Audio conversion failed: %v", err)
processingError = true
return
}
processedAudio = append(processedAudio, ProcessedAudioFile{
Path: opusFile,
Language: lang,
Title: trackTitle,
Delay: delay,
})
}
}
LogInfo("--- Finished Audio Processing ---")
LogInfo("Assembling final file with mkvmerge...")
mkvArgs := []string{"-o", intermediateOutput, encodedVideoFile}
for _, aud := range processedAudio {
if aud.Delay != 0 {
mkvArgs = append(mkvArgs, "--sync", fmt.Sprintf("0:%d", aud.Delay))
}
mkvArgs = append(mkvArgs, "--language", fmt.Sprintf("0:%s", aud.Language))
mkvArgs = append(mkvArgs, "--track-name", fmt.Sprintf("0:%s", aud.Title))
mkvArgs = append(mkvArgs, aud.Path)
}
mkvArgs = append(mkvArgs, "--no-video")
if len(remuxAudioTracks) > 0 {
mkvArgs = append(mkvArgs, "--audio-tracks", strings.Join(remuxAudioTracks, ","))
} else {
mkvArgs = append(mkvArgs, "--no-audio")
}
mkvArgs = append(mkvArgs, absPath)
_, err = RunCmd("mkvmerge", mkvArgs, false)
if err != nil {
LogInfo("mkvmerge assembly failed: %v", err)
processingError = true
return
}
LogInfo("Moving files to final destinations...")
err = os.Rename(filePath, filepath.Join(DirOriginal, filepath.Base(filePath)))
if err != nil {
LogInfo("Failed to move original file: %v", err)
}
err = os.Rename(intermediateOutput, filepath.Join(DirCompleted, filepath.Base(filePath)))
if err != nil {
LogInfo("Failed to move completed file: %v", err)
}
LogInfo("Cleaning up persistent video temporary files...")
temps := []string{
baseName + ".vpy",
baseName + ".ut.mkv",
encodedVideoFile,
baseName + ".ut.mkv.lwi",
baseName + ".ut.mkv.ffindex",
}
if handbrakeCleanup != "" {
temps = append(temps, handbrakeCleanup)
}
for _, t := range temps {
if _, err := os.Stat(t); err == nil {
LogInfo(" Deleting: %s", t)
os.Remove(t)
}
}
}