diff --git a/main.go b/main.go index 1a2b0fb..defc1d9 100644 --- a/main.go +++ b/main.go @@ -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()