Avoid batch remux output collisions

This commit is contained in:
Stu Leak 2025-12-24 03:02:24 -05:00
parent 834d6b5517
commit e22eae8207

110
main.go
View File

@ -1995,9 +1995,10 @@ func (s *appState) addAllConvertToQueue() (int, error) {
return 0, fmt.Errorf("no videos loaded")
}
usedOutputs := make(map[string]struct{})
count := 0
for _, src := range s.loadedVideos {
if err := s.addConvertToQueueForSource(src); err != nil {
if err := s.addConvertToQueueForSourceWithOutputs(src, usedOutputs); err != nil {
return count, fmt.Errorf("failed to add %s: %w", filepath.Base(src.Path), err)
}
count++
@ -2006,6 +2007,113 @@ func (s *appState) addAllConvertToQueue() (int, error) {
return count, nil
}
func (s *appState) addConvertToQueueForSourceWithOutputs(src *videoSource, used map[string]struct{}) error {
outputBase := s.resolveOutputBase(src, true)
cfg := s.convert
cfg.OutputBase = outputBase
outDir := filepath.Dir(src.Path)
outName := cfg.OutputFile()
if outName == "" {
outName = "converted" + cfg.SelectedFormat.Ext
}
outPath := filepath.Join(outDir, outName)
if outPath == src.Path {
outPath = filepath.Join(outDir, "converted-"+outName)
}
// Ensure unique output path within batch to avoid overwrites.
ext := filepath.Ext(outPath)
base := strings.TrimSuffix(outPath, ext)
candidate := outPath
for i := 2; ; i++ {
if _, ok := used[candidate]; !ok {
if _, err := os.Stat(candidate); os.IsNotExist(err) {
break
}
}
candidate = fmt.Sprintf("%s-%d%s", base, i, ext)
}
outPath = candidate
used[outPath] = struct{}{}
// Align codec choice with the selected format when the preset implies a codec change.
adjustedCodec := s.convert.VideoCodec
if preset := s.convert.SelectedFormat.VideoCodec; preset != "" {
if friendly := friendlyCodecFromPreset(preset); friendly != "" {
if adjustedCodec == "" ||
(strings.EqualFold(adjustedCodec, "H.264") && friendly == "H.265") ||
(strings.EqualFold(adjustedCodec, "H.265") && friendly == "H.264") {
adjustedCodec = friendly
}
}
}
// Create job config map
config := map[string]interface{}{
"inputPath": src.Path,
"outputPath": outPath,
"outputBase": cfg.OutputBase,
"selectedFormat": cfg.SelectedFormat,
"quality": cfg.Quality,
"mode": cfg.Mode,
"preserveChapters": cfg.PreserveChapters,
"videoCodec": adjustedCodec,
"encoderPreset": cfg.EncoderPreset,
"crf": cfg.CRF,
"bitrateMode": cfg.BitrateMode,
"bitratePreset": cfg.BitratePreset,
"videoBitrate": cfg.VideoBitrate,
"targetFileSize": cfg.TargetFileSize,
"targetResolution": cfg.TargetResolution,
"frameRate": cfg.FrameRate,
"pixelFormat": cfg.PixelFormat,
"hardwareAccel": cfg.HardwareAccel,
"twoPass": cfg.TwoPass,
"h264Profile": cfg.H264Profile,
"h264Level": cfg.H264Level,
"deinterlace": cfg.Deinterlace,
"deinterlaceMethod": cfg.DeinterlaceMethod,
"autoCrop": cfg.AutoCrop,
"cropWidth": cfg.CropWidth,
"cropHeight": cfg.CropHeight,
"cropX": cfg.CropX,
"cropY": cfg.CropY,
"flipHorizontal": cfg.FlipHorizontal,
"flipVertical": cfg.FlipVertical,
"rotation": cfg.Rotation,
"audioCodec": cfg.AudioCodec,
"audioBitrate": cfg.AudioBitrate,
"audioChannels": cfg.AudioChannels,
"audioSampleRate": cfg.AudioSampleRate,
"normalizeAudio": cfg.NormalizeAudio,
"inverseTelecine": cfg.InverseTelecine,
"coverArtPath": cfg.CoverArtPath,
"aspectHandling": cfg.AspectHandling,
"outputAspect": cfg.OutputAspect,
"sourceWidth": src.Width,
"sourceHeight": src.Height,
"sourceDuration": src.Duration,
"sourceBitrate": src.Bitrate,
"fieldOrder": src.FieldOrder,
"autoCompare": s.autoCompare,
}
job := &queue.Job{
Type: queue.JobTypeConvert,
Title: fmt.Sprintf("Convert %s", filepath.Base(src.Path)),
Description: fmt.Sprintf("Output: %s → %s", utils.ShortenMiddle(filepath.Base(src.Path), 40), utils.ShortenMiddle(filepath.Base(outPath), 40)),
InputFile: src.Path,
OutputFile: outPath,
Config: config,
}
s.jobQueue.Add(job)
logging.Debug(logging.CatSystem, "added convert job to queue: %s", job.ID)
return nil
}
func (s *appState) showBenchmark() {
s.stopPreview()
s.stopPlayer()