Compare commits
5 Commits
0779016616
...
e84dfd5eed
| Author | SHA1 | Date | |
|---|---|---|---|
| e84dfd5eed | |||
| ff612b547c | |||
| de70448897 | |||
| 1491d0b0c0 | |||
| fe5d0f7f87 |
360
main.go
360
main.go
|
|
@ -504,6 +504,7 @@ type convertConfig struct {
|
|||
Mode string // Simple or Advanced
|
||||
UseAutoNaming bool
|
||||
AutoNameTemplate string // Template for metadata-driven naming, e.g., "<actress> - <studio> - <scene>"
|
||||
PreserveChapters bool
|
||||
|
||||
// Video encoding settings
|
||||
VideoCodec string // H.264, H.265, VP9, AV1, Copy
|
||||
|
|
@ -572,6 +573,7 @@ func defaultConvertConfig() convertConfig {
|
|||
Mode: "Simple",
|
||||
UseAutoNaming: false,
|
||||
AutoNameTemplate: "<actress> - <studio> - <scene>",
|
||||
PreserveChapters: true,
|
||||
|
||||
VideoCodec: "H.264",
|
||||
EncoderPreset: "slow",
|
||||
|
|
@ -1929,6 +1931,7 @@ func (s *appState) addConvertToQueueForSource(src *videoSource) error {
|
|||
"selectedFormat": cfg.SelectedFormat,
|
||||
"quality": cfg.Quality,
|
||||
"mode": cfg.Mode,
|
||||
"preserveChapters": cfg.PreserveChapters,
|
||||
"videoCodec": adjustedCodec,
|
||||
"encoderPreset": cfg.EncoderPreset,
|
||||
"crf": cfg.CRF,
|
||||
|
|
@ -1965,6 +1968,7 @@ func (s *appState) addConvertToQueueForSource(src *videoSource) error {
|
|||
"sourceWidth": src.Width,
|
||||
"sourceHeight": src.Height,
|
||||
"sourceDuration": src.Duration,
|
||||
"sourceBitrate": src.Bitrate,
|
||||
"fieldOrder": src.FieldOrder,
|
||||
"autoCompare": s.autoCompare, // Include auto-compare flag
|
||||
}
|
||||
|
|
@ -2672,9 +2676,10 @@ func (s *appState) batchAddToQueue(paths []string) {
|
|||
"outputPath": outPath,
|
||||
"outputBase": outputBase,
|
||||
"selectedFormat": s.convert.SelectedFormat,
|
||||
"quality": s.convert.Quality,
|
||||
"mode": s.convert.Mode,
|
||||
"videoCodec": s.convert.VideoCodec,
|
||||
"quality": s.convert.Quality,
|
||||
"mode": s.convert.Mode,
|
||||
"preserveChapters": s.convert.PreserveChapters,
|
||||
"videoCodec": s.convert.VideoCodec,
|
||||
"encoderPreset": s.convert.EncoderPreset,
|
||||
"crf": s.convert.CRF,
|
||||
"bitrateMode": s.convert.BitrateMode,
|
||||
|
|
@ -2701,7 +2706,7 @@ func (s *appState) batchAddToQueue(paths []string) {
|
|||
"sourceWidth": src.Width,
|
||||
"sourceHeight": src.Height,
|
||||
"sourceBitrate": src.Bitrate,
|
||||
"sourceDuration": src.Duration,
|
||||
"sourceDuration": src.Duration,
|
||||
"fieldOrder": src.FieldOrder,
|
||||
}
|
||||
|
||||
|
|
@ -3689,6 +3694,10 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
}
|
||||
}
|
||||
isDVD := selectedFormat.Ext == ".mpg"
|
||||
remux := strings.EqualFold(selectedFormat.VideoCodec, "copy")
|
||||
if vc, ok := cfg["videoCodec"].(string); ok && strings.EqualFold(vc, "Copy") {
|
||||
remux = true
|
||||
}
|
||||
|
||||
// DVD presets: enforce compliant codecs and audio settings
|
||||
// Note: We do NOT force resolution - user can choose Source or specific resolution
|
||||
|
|
@ -3712,6 +3721,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
cfg["pixelFormat"] = "yuv420p"
|
||||
}
|
||||
|
||||
if remux {
|
||||
args = append(args, "-fflags", "+genpts")
|
||||
}
|
||||
args = append(args, "-i", inputPath)
|
||||
|
||||
// Add cover art if available
|
||||
|
|
@ -3746,155 +3758,156 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
}
|
||||
}
|
||||
|
||||
// Video filters
|
||||
var vf []string
|
||||
|
||||
// Deinterlacing
|
||||
shouldDeinterlace := false
|
||||
deinterlaceMode, _ := cfg["deinterlace"].(string)
|
||||
fieldOrder, _ := cfg["fieldOrder"].(string)
|
||||
|
||||
if deinterlaceMode == "Force" {
|
||||
shouldDeinterlace = true
|
||||
} else if deinterlaceMode == "Auto" || deinterlaceMode == "" {
|
||||
// Auto-detect based on field order
|
||||
if fieldOrder != "" && fieldOrder != "progressive" && fieldOrder != "unknown" {
|
||||
shouldDeinterlace = true
|
||||
}
|
||||
}
|
||||
|
||||
// Legacy support
|
||||
if inverseTelecine, _ := cfg["inverseTelecine"].(bool); inverseTelecine {
|
||||
shouldDeinterlace = true
|
||||
}
|
||||
|
||||
if shouldDeinterlace {
|
||||
// Choose deinterlacing method
|
||||
deintMethod, _ := cfg["deinterlaceMethod"].(string)
|
||||
if deintMethod == "" {
|
||||
deintMethod = "bwdif" // Default to bwdif (higher quality)
|
||||
}
|
||||
|
||||
if deintMethod == "bwdif" {
|
||||
vf = append(vf, "bwdif=mode=send_frame:parity=auto")
|
||||
} else {
|
||||
vf = append(vf, "yadif=0:-1:0")
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-crop black bars (apply before scaling for best results)
|
||||
if autoCrop, _ := cfg["autoCrop"].(bool); autoCrop {
|
||||
cropWidth, _ := cfg["cropWidth"].(string)
|
||||
cropHeight, _ := cfg["cropHeight"].(string)
|
||||
cropX, _ := cfg["cropX"].(string)
|
||||
cropY, _ := cfg["cropY"].(string)
|
||||
|
||||
if cropWidth != "" && cropHeight != "" {
|
||||
cropW := strings.TrimSpace(cropWidth)
|
||||
cropH := strings.TrimSpace(cropHeight)
|
||||
cropXStr := strings.TrimSpace(cropX)
|
||||
cropYStr := strings.TrimSpace(cropY)
|
||||
|
||||
// Default to center crop if X/Y not specified
|
||||
if cropXStr == "" {
|
||||
cropXStr = "(in_w-out_w)/2"
|
||||
}
|
||||
if cropYStr == "" {
|
||||
cropYStr = "(in_h-out_h)/2"
|
||||
}
|
||||
|
||||
cropFilter := fmt.Sprintf("crop=%s:%s:%s:%s", cropW, cropH, cropXStr, cropYStr)
|
||||
vf = append(vf, cropFilter)
|
||||
logging.Debug(logging.CatFFMPEG, "applying crop in queue job: %s", cropFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// Scaling/Resolution
|
||||
targetResolution, _ := cfg["targetResolution"].(string)
|
||||
if targetResolution != "" && targetResolution != "Source" {
|
||||
var scaleFilter string
|
||||
switch targetResolution {
|
||||
case "360p":
|
||||
scaleFilter = "scale=-2:360"
|
||||
case "480p":
|
||||
scaleFilter = "scale=-2:480"
|
||||
case "540p":
|
||||
scaleFilter = "scale=-2:540"
|
||||
case "720p":
|
||||
scaleFilter = "scale=-2:720"
|
||||
case "1080p":
|
||||
scaleFilter = "scale=-2:1080"
|
||||
case "1440p":
|
||||
scaleFilter = "scale=-2:1440"
|
||||
case "4K":
|
||||
scaleFilter = "scale=-2:2160"
|
||||
case "8K":
|
||||
scaleFilter = "scale=-2:4320"
|
||||
}
|
||||
if scaleFilter != "" {
|
||||
vf = append(vf, scaleFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// Aspect ratio conversion
|
||||
// Source metrics (used for filters and bitrate defaults)
|
||||
sourceWidth, _ := cfg["sourceWidth"].(int)
|
||||
sourceHeight, _ := cfg["sourceHeight"].(int)
|
||||
// Get source bitrate if present
|
||||
sourceBitrate := 0
|
||||
if v, ok := cfg["sourceBitrate"].(float64); ok {
|
||||
sourceBitrate = int(v)
|
||||
}
|
||||
srcAspect := utils.AspectRatioFloat(sourceWidth, sourceHeight)
|
||||
outputAspect, _ := cfg["outputAspect"].(string)
|
||||
aspectHandling, _ := cfg["aspectHandling"].(string)
|
||||
|
||||
// Create temp source for aspect calculation
|
||||
tempSrc := &videoSource{Width: sourceWidth, Height: sourceHeight}
|
||||
targetAspect := resolveTargetAspect(outputAspect, tempSrc)
|
||||
if targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01) {
|
||||
vf = append(vf, aspectFilters(targetAspect, aspectHandling)...)
|
||||
}
|
||||
// Video filters
|
||||
var vf []string
|
||||
if !remux {
|
||||
// Deinterlacing
|
||||
shouldDeinterlace := false
|
||||
deinterlaceMode, _ := cfg["deinterlace"].(string)
|
||||
fieldOrder, _ := cfg["fieldOrder"].(string)
|
||||
|
||||
// Flip horizontal
|
||||
flipH, _ := cfg["flipHorizontal"].(bool)
|
||||
if flipH {
|
||||
vf = append(vf, "hflip")
|
||||
}
|
||||
|
||||
// Flip vertical
|
||||
flipV, _ := cfg["flipVertical"].(bool)
|
||||
if flipV {
|
||||
vf = append(vf, "vflip")
|
||||
}
|
||||
|
||||
// Rotation
|
||||
rotation, _ := cfg["rotation"].(string)
|
||||
if rotation != "" && rotation != "0" {
|
||||
switch rotation {
|
||||
case "90":
|
||||
vf = append(vf, "transpose=1") // 90 degrees clockwise
|
||||
case "180":
|
||||
vf = append(vf, "transpose=1,transpose=1") // 180 degrees
|
||||
case "270":
|
||||
vf = append(vf, "transpose=2") // 90 degrees counter-clockwise (= 270 clockwise)
|
||||
if deinterlaceMode == "Force" {
|
||||
shouldDeinterlace = true
|
||||
} else if deinterlaceMode == "Auto" || deinterlaceMode == "" {
|
||||
// Auto-detect based on field order
|
||||
if fieldOrder != "" && fieldOrder != "progressive" && fieldOrder != "unknown" {
|
||||
shouldDeinterlace = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Frame rate
|
||||
frameRate, _ := cfg["frameRate"].(string)
|
||||
useMotionInterp, _ := cfg["useMotionInterpolation"].(bool)
|
||||
if frameRate != "" && frameRate != "Source" {
|
||||
if useMotionInterp {
|
||||
// Use motion interpolation for smooth frame rate changes
|
||||
vf = append(vf, fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1", frameRate))
|
||||
} else {
|
||||
// Simple frame rate change (duplicates/drops frames)
|
||||
vf = append(vf, "fps="+frameRate)
|
||||
// Legacy support
|
||||
if inverseTelecine, _ := cfg["inverseTelecine"].(bool); inverseTelecine {
|
||||
shouldDeinterlace = true
|
||||
}
|
||||
}
|
||||
|
||||
if len(vf) > 0 {
|
||||
args = append(args, "-vf", strings.Join(vf, ","))
|
||||
if shouldDeinterlace {
|
||||
// Choose deinterlacing method
|
||||
deintMethod, _ := cfg["deinterlaceMethod"].(string)
|
||||
if deintMethod == "" {
|
||||
deintMethod = "bwdif" // Default to bwdif (higher quality)
|
||||
}
|
||||
|
||||
if deintMethod == "bwdif" {
|
||||
vf = append(vf, "bwdif=mode=send_frame:parity=auto")
|
||||
} else {
|
||||
vf = append(vf, "yadif=0:-1:0")
|
||||
}
|
||||
}
|
||||
|
||||
// Auto-crop black bars (apply before scaling for best results)
|
||||
if autoCrop, _ := cfg["autoCrop"].(bool); autoCrop {
|
||||
cropWidth, _ := cfg["cropWidth"].(string)
|
||||
cropHeight, _ := cfg["cropHeight"].(string)
|
||||
cropX, _ := cfg["cropX"].(string)
|
||||
cropY, _ := cfg["cropY"].(string)
|
||||
|
||||
if cropWidth != "" && cropHeight != "" {
|
||||
cropW := strings.TrimSpace(cropWidth)
|
||||
cropH := strings.TrimSpace(cropHeight)
|
||||
cropXStr := strings.TrimSpace(cropX)
|
||||
cropYStr := strings.TrimSpace(cropY)
|
||||
|
||||
// Default to center crop if X/Y not specified
|
||||
if cropXStr == "" {
|
||||
cropXStr = "(in_w-out_w)/2"
|
||||
}
|
||||
if cropYStr == "" {
|
||||
cropYStr = "(in_h-out_h)/2"
|
||||
}
|
||||
|
||||
cropFilter := fmt.Sprintf("crop=%s:%s:%s:%s", cropW, cropH, cropXStr, cropYStr)
|
||||
vf = append(vf, cropFilter)
|
||||
logging.Debug(logging.CatFFMPEG, "applying crop in queue job: %s", cropFilter)
|
||||
}
|
||||
}
|
||||
|
||||
// Scaling/Resolution
|
||||
targetResolution, _ := cfg["targetResolution"].(string)
|
||||
if targetResolution != "" && targetResolution != "Source" {
|
||||
var scaleFilter string
|
||||
switch targetResolution {
|
||||
case "360p":
|
||||
scaleFilter = "scale=-2:360"
|
||||
case "480p":
|
||||
scaleFilter = "scale=-2:480"
|
||||
case "540p":
|
||||
scaleFilter = "scale=-2:540"
|
||||
case "720p":
|
||||
scaleFilter = "scale=-2:720"
|
||||
case "1080p":
|
||||
scaleFilter = "scale=-2:1080"
|
||||
case "1440p":
|
||||
scaleFilter = "scale=-2:1440"
|
||||
case "4K":
|
||||
scaleFilter = "scale=-2:2160"
|
||||
case "8K":
|
||||
scaleFilter = "scale=-2:4320"
|
||||
}
|
||||
if scaleFilter != "" {
|
||||
vf = append(vf, scaleFilter)
|
||||
}
|
||||
}
|
||||
// Aspect ratio conversion
|
||||
srcAspect := utils.AspectRatioFloat(sourceWidth, sourceHeight)
|
||||
outputAspect, _ := cfg["outputAspect"].(string)
|
||||
aspectHandling, _ := cfg["aspectHandling"].(string)
|
||||
|
||||
// Create temp source for aspect calculation
|
||||
tempSrc := &videoSource{Width: sourceWidth, Height: sourceHeight}
|
||||
targetAspect := resolveTargetAspect(outputAspect, tempSrc)
|
||||
if targetAspect > 0 && srcAspect > 0 && !utils.RatiosApproxEqual(targetAspect, srcAspect, 0.01) {
|
||||
vf = append(vf, aspectFilters(targetAspect, aspectHandling)...)
|
||||
}
|
||||
|
||||
// Flip horizontal
|
||||
flipH, _ := cfg["flipHorizontal"].(bool)
|
||||
if flipH {
|
||||
vf = append(vf, "hflip")
|
||||
}
|
||||
|
||||
// Flip vertical
|
||||
flipV, _ := cfg["flipVertical"].(bool)
|
||||
if flipV {
|
||||
vf = append(vf, "vflip")
|
||||
}
|
||||
|
||||
// Rotation
|
||||
rotation, _ := cfg["rotation"].(string)
|
||||
if rotation != "" && rotation != "0" {
|
||||
switch rotation {
|
||||
case "90":
|
||||
vf = append(vf, "transpose=1") // 90 degrees clockwise
|
||||
case "180":
|
||||
vf = append(vf, "transpose=1,transpose=1") // 180 degrees
|
||||
case "270":
|
||||
vf = append(vf, "transpose=2") // 90 degrees counter-clockwise (= 270 clockwise)
|
||||
}
|
||||
}
|
||||
|
||||
// Frame rate
|
||||
frameRate, _ := cfg["frameRate"].(string)
|
||||
useMotionInterp, _ := cfg["useMotionInterpolation"].(bool)
|
||||
if frameRate != "" && frameRate != "Source" {
|
||||
if useMotionInterp {
|
||||
// Use motion interpolation for smooth frame rate changes
|
||||
vf = append(vf, fmt.Sprintf("minterpolate=fps=%s:mi_mode=mci:mc_mode=aobmc:me_mode=bidir:vsbmc=1", frameRate))
|
||||
} else {
|
||||
// Simple frame rate change (duplicates/drops frames)
|
||||
vf = append(vf, "fps="+frameRate)
|
||||
}
|
||||
}
|
||||
|
||||
if len(vf) > 0 {
|
||||
args = append(args, "-vf", strings.Join(vf, ","))
|
||||
}
|
||||
}
|
||||
|
||||
// Video codec
|
||||
|
|
@ -4104,7 +4117,16 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
}
|
||||
|
||||
// Preserve chapters and metadata
|
||||
args = append(args, "-map_chapters", "0", "-map_metadata", "0")
|
||||
preserveChapters := true
|
||||
if v, ok := cfg["preserveChapters"].(bool); ok {
|
||||
preserveChapters = v
|
||||
}
|
||||
if preserveChapters {
|
||||
args = append(args, "-map_chapters", "0")
|
||||
} else {
|
||||
args = append(args, "-map_chapters", "-1")
|
||||
}
|
||||
args = append(args, "-map_metadata", "0")
|
||||
|
||||
// Copy subtitle streams by default (don't re-encode)
|
||||
args = append(args, "-c:s", "copy")
|
||||
|
|
@ -4117,14 +4139,18 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
|||
// DVD-specific parameters are set manually in the video codec section below.
|
||||
|
||||
// Fix VFR/desync issues - regenerate timestamps and enforce CFR
|
||||
args = append(args, "-fflags", "+genpts")
|
||||
frameRateStr, _ := cfg["frameRate"].(string)
|
||||
sourceDuration, _ := cfg["sourceDuration"].(float64)
|
||||
if frameRateStr != "" && frameRateStr != "Source" {
|
||||
args = append(args, "-r", frameRateStr)
|
||||
} else if sourceDuration > 0 {
|
||||
// Calculate approximate source frame rate if available
|
||||
args = append(args, "-r", "30") // Safe default
|
||||
if !remux {
|
||||
args = append(args, "-fflags", "+genpts")
|
||||
frameRateStr, _ := cfg["frameRate"].(string)
|
||||
sourceDuration, _ := cfg["sourceDuration"].(float64)
|
||||
if frameRateStr != "" && frameRateStr != "Source" {
|
||||
args = append(args, "-r", frameRateStr)
|
||||
} else if sourceDuration > 0 {
|
||||
// Calculate approximate source frame rate if available
|
||||
args = append(args, "-r", "30") // Safe default
|
||||
}
|
||||
} else {
|
||||
args = append(args, "-avoid_negative_ts", "make_zero")
|
||||
}
|
||||
|
||||
// Progress feed
|
||||
|
|
@ -5880,6 +5906,12 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
dvdAspectBox := container.NewVBox(dvdAspectLabel, dvdAspectSelect, dvdInfoLabel)
|
||||
dvdAspectBox.Hide() // Hidden by default
|
||||
|
||||
// Chapter preservation
|
||||
preserveChaptersCheck := widget.NewCheck("Keep chapters", func(checked bool) {
|
||||
state.convert.PreserveChapters = checked
|
||||
})
|
||||
preserveChaptersCheck.SetChecked(state.convert.PreserveChapters)
|
||||
|
||||
// Placeholder for updateDVDOptions - will be defined after resolution/framerate selects are created
|
||||
var updateDVDOptions func()
|
||||
|
||||
|
|
@ -7559,6 +7591,31 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
audioCodecSelect.Enable()
|
||||
}
|
||||
}
|
||||
if remux {
|
||||
state.convert.AspectUserSet = false
|
||||
if syncAspect != nil {
|
||||
syncAspect("Source", false)
|
||||
}
|
||||
if targetAspectSelectSimple != nil {
|
||||
targetAspectSelectSimple.Disable()
|
||||
}
|
||||
if targetAspectSelect != nil {
|
||||
targetAspectSelect.Disable()
|
||||
}
|
||||
aspectOptions.Disable()
|
||||
aspectBox.Hide()
|
||||
} else {
|
||||
if targetAspectSelectSimple != nil {
|
||||
targetAspectSelectSimple.Enable()
|
||||
}
|
||||
if targetAspectSelect != nil {
|
||||
targetAspectSelect.Enable()
|
||||
}
|
||||
aspectOptions.Enable()
|
||||
if updateAspectBoxVisibility != nil {
|
||||
updateAspectBoxVisibility()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
simpleEncodingSection = container.NewVBox(
|
||||
|
|
@ -7578,6 +7635,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
formatSelect,
|
||||
chapterWarningLabel, // Warning when converting chapters to DVD
|
||||
preserveChaptersCheck,
|
||||
dvdAspectBox, // DVD options appear here when DVD format selected
|
||||
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
outputEntry,
|
||||
|
|
@ -7639,6 +7697,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
widget.NewLabelWithStyle("Format", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
formatSelect,
|
||||
chapterWarningLabel, // Warning when converting chapters to DVD
|
||||
preserveChaptersCheck,
|
||||
dvdAspectBox, // DVD options appear here when DVD format selected
|
||||
widget.NewLabelWithStyle("Output Name", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
outputEntry,
|
||||
|
|
@ -7709,6 +7768,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
|||
autoNameTemplate.SetText(state.convert.AutoNameTemplate)
|
||||
outputEntry.SetText(state.convert.OutputBase)
|
||||
outputHint.SetText(fmt.Sprintf("Output file: %s", state.convert.OutputFile()))
|
||||
preserveChaptersCheck.SetChecked(state.convert.PreserveChapters)
|
||||
resolutionSelectSimple.SetSelected(state.convert.TargetResolution)
|
||||
resolutionSelect.SetSelected(state.convert.TargetResolution)
|
||||
frameRateSelect.SetSelected(state.convert.FrameRate)
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user