Enforce DVD presets and optional merge chapters
This commit is contained in:
parent
dd9e4a8afa
commit
0c86d9c793
|
|
@ -51,12 +51,10 @@ func BuildMainMenu(modules []ModuleInfo, onModuleClick func(string), onModuleDro
|
||||||
onModuleClick(id)
|
onModuleClick(id)
|
||||||
}
|
}
|
||||||
dropFunc = func(items []fyne.URI) {
|
dropFunc = func(items []fyne.URI) {
|
||||||
fmt.Printf("[MAINMENU] dropFunc called for module=%s itemCount=%d\n", id, len(items))
|
|
||||||
logging.Debug(logging.CatUI, "MainMenu dropFunc called for module=%s itemCount=%d", id, len(items))
|
logging.Debug(logging.CatUI, "MainMenu dropFunc called for module=%s itemCount=%d", id, len(items))
|
||||||
onModuleDrop(id, items)
|
onModuleDrop(id, items)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
fmt.Printf("[MAINMENU] Creating tile for module=%s enabled=%v hasDropFunc=%v\n", modID, mod.Enabled, dropFunc != nil)
|
|
||||||
logging.Debug(logging.CatUI, "Creating tile for module=%s enabled=%v hasDropFunc=%v", modID, mod.Enabled, dropFunc != nil)
|
logging.Debug(logging.CatUI, "Creating tile for module=%s enabled=%v hasDropFunc=%v", modID, mod.Enabled, dropFunc != nil)
|
||||||
categorized[cat] = append(categorized[cat], buildModuleTile(mod, tapFunc, dropFunc))
|
categorized[cat] = append(categorized[cat], buildModuleTile(mod, tapFunc, dropFunc))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
135
main.go
135
main.go
|
|
@ -555,6 +555,7 @@ type appState struct {
|
||||||
mergeOutput string
|
mergeOutput string
|
||||||
mergeKeepAll bool
|
mergeKeepAll bool
|
||||||
mergeCodecMode string
|
mergeCodecMode string
|
||||||
|
mergeChapters bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type mergeClip struct {
|
type mergeClip struct {
|
||||||
|
|
@ -1672,6 +1673,11 @@ func (s *appState) showMergeView() {
|
||||||
})
|
})
|
||||||
keepAllCheck.SetChecked(s.mergeKeepAll)
|
keepAllCheck.SetChecked(s.mergeKeepAll)
|
||||||
|
|
||||||
|
chapterCheck := widget.NewCheck("Create chapters from each clip", func(v bool) {
|
||||||
|
s.mergeChapters = v
|
||||||
|
})
|
||||||
|
chapterCheck.SetChecked(s.mergeChapters)
|
||||||
|
|
||||||
codecModeSelect := widget.NewSelect([]string{"Copy (if compatible)", "Re-encode (H.265)"}, func(val string) {
|
codecModeSelect := widget.NewSelect([]string{"Copy (if compatible)", "Re-encode (H.265)"}, func(val string) {
|
||||||
if strings.HasPrefix(val, "Copy") {
|
if strings.HasPrefix(val, "Copy") {
|
||||||
s.mergeCodecMode = "copy"
|
s.mergeCodecMode = "copy"
|
||||||
|
|
@ -1745,6 +1751,7 @@ func (s *appState) showMergeView() {
|
||||||
formatSelect,
|
formatSelect,
|
||||||
codecModeSelect,
|
codecModeSelect,
|
||||||
keepAllCheck,
|
keepAllCheck,
|
||||||
|
chapterCheck,
|
||||||
widget.NewSeparator(),
|
widget.NewSeparator(),
|
||||||
widget.NewLabel("Output Path"),
|
widget.NewLabel("Output Path"),
|
||||||
container.NewBorder(nil, nil, nil, browseOut, outputEntry),
|
container.NewBorder(nil, nil, nil, browseOut, outputEntry),
|
||||||
|
|
@ -1784,6 +1791,7 @@ func (s *appState) addMergeToQueue(startNow bool) error {
|
||||||
"clips": clips,
|
"clips": clips,
|
||||||
"format": s.mergeFormat,
|
"format": s.mergeFormat,
|
||||||
"keepAllStreams": s.mergeKeepAll,
|
"keepAllStreams": s.mergeKeepAll,
|
||||||
|
"chapters": s.mergeChapters,
|
||||||
"codecMode": s.mergeCodecMode,
|
"codecMode": s.mergeCodecMode,
|
||||||
"outputPath": s.mergeOutput,
|
"outputPath": s.mergeOutput,
|
||||||
}
|
}
|
||||||
|
|
@ -1840,6 +1848,10 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
||||||
cfg := job.Config
|
cfg := job.Config
|
||||||
format, _ := cfg["format"].(string)
|
format, _ := cfg["format"].(string)
|
||||||
keepAll, _ := cfg["keepAllStreams"].(bool)
|
keepAll, _ := cfg["keepAllStreams"].(bool)
|
||||||
|
withChapters, ok := cfg["chapters"].(bool)
|
||||||
|
if !ok {
|
||||||
|
withChapters = true
|
||||||
|
}
|
||||||
codecMode, _ := cfg["codecMode"].(string) // copy or encode
|
codecMode, _ := cfg["codecMode"].(string) // copy or encode
|
||||||
outputPath, _ := cfg["outputPath"].(string)
|
outputPath, _ := cfg["outputPath"].(string)
|
||||||
|
|
||||||
|
|
@ -1869,28 +1881,31 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
||||||
}
|
}
|
||||||
_ = listFile.Close()
|
_ = listFile.Close()
|
||||||
|
|
||||||
// Build chapters metadata
|
var chapterFile *os.File
|
||||||
chapterFile, err := os.CreateTemp(tmpDir, "vt-merge-chapters-*.txt")
|
if withChapters {
|
||||||
if err != nil {
|
chapterFile, err = os.CreateTemp(tmpDir, "vt-merge-chapters-*.txt")
|
||||||
return err
|
if err != nil {
|
||||||
}
|
return err
|
||||||
var elapsed float64
|
|
||||||
fmt.Fprintln(chapterFile, ";FFMETADATA1")
|
|
||||||
for i, c := range clips {
|
|
||||||
startMs := int64(elapsed * 1000)
|
|
||||||
endMs := int64((elapsed + c.Duration) * 1000)
|
|
||||||
fmt.Fprintln(chapterFile, "[CHAPTER]")
|
|
||||||
fmt.Fprintln(chapterFile, "TIMEBASE=1/1000")
|
|
||||||
fmt.Fprintf(chapterFile, "START=%d\n", startMs)
|
|
||||||
fmt.Fprintf(chapterFile, "END=%d\n", endMs)
|
|
||||||
name := c.Chapter
|
|
||||||
if strings.TrimSpace(name) == "" {
|
|
||||||
name = fmt.Sprintf("Part %d", i+1)
|
|
||||||
}
|
}
|
||||||
fmt.Fprintf(chapterFile, "title=%s\n", name)
|
var elapsed float64
|
||||||
elapsed += c.Duration
|
fmt.Fprintln(chapterFile, ";FFMETADATA1")
|
||||||
|
for i, c := range clips {
|
||||||
|
startMs := int64(elapsed * 1000)
|
||||||
|
endMs := int64((elapsed + c.Duration) * 1000)
|
||||||
|
fmt.Fprintln(chapterFile, "[CHAPTER]")
|
||||||
|
fmt.Fprintln(chapterFile, "TIMEBASE=1/1000")
|
||||||
|
fmt.Fprintf(chapterFile, "START=%d\n", startMs)
|
||||||
|
fmt.Fprintf(chapterFile, "END=%d\n", endMs)
|
||||||
|
name := c.Chapter
|
||||||
|
if strings.TrimSpace(name) == "" {
|
||||||
|
name = fmt.Sprintf("Part %d", i+1)
|
||||||
|
}
|
||||||
|
fmt.Fprintf(chapterFile, "title=%s\n", name)
|
||||||
|
elapsed += c.Duration
|
||||||
|
}
|
||||||
|
_ = chapterFile.Close()
|
||||||
|
defer os.Remove(chapterFile.Name())
|
||||||
}
|
}
|
||||||
_ = chapterFile.Close()
|
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-y",
|
"-y",
|
||||||
|
|
@ -1899,9 +1914,9 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
||||||
"-f", "concat",
|
"-f", "concat",
|
||||||
"-safe", "0",
|
"-safe", "0",
|
||||||
"-i", listFile.Name(),
|
"-i", listFile.Name(),
|
||||||
"-i", chapterFile.Name(),
|
}
|
||||||
"-map_metadata", "1",
|
if withChapters && chapterFile != nil {
|
||||||
"-map_chapters", "1",
|
args = append(args, "-i", chapterFile.Name(), "-map_metadata", "1", "-map_chapters", "1")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Map streams
|
// Map streams
|
||||||
|
|
@ -2622,6 +2637,10 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
||||||
center := math.Max(0, src.Duration/2-10)
|
center := math.Max(0, src.Duration/2-10)
|
||||||
start := fmt.Sprintf("%.2f", center)
|
start := fmt.Sprintf("%.2f", center)
|
||||||
|
|
||||||
|
if progressCallback != nil {
|
||||||
|
progressCallback(0)
|
||||||
|
}
|
||||||
|
|
||||||
args := []string{
|
args := []string{
|
||||||
"-y",
|
"-y",
|
||||||
"-hide_banner",
|
"-hide_banner",
|
||||||
|
|
@ -2649,6 +2668,12 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
||||||
scaleFilter = "scale=-2:2160"
|
scaleFilter = "scale=-2:2160"
|
||||||
case "8K":
|
case "8K":
|
||||||
scaleFilter = "scale=-2:4320"
|
scaleFilter = "scale=-2:4320"
|
||||||
|
case "NTSC (720×480)":
|
||||||
|
scaleFilter = "scale=720:480"
|
||||||
|
case "PAL (720×540)":
|
||||||
|
scaleFilter = "scale=720:540"
|
||||||
|
case "PAL (720×576)":
|
||||||
|
scaleFilter = "scale=720:576"
|
||||||
}
|
}
|
||||||
if scaleFilter != "" {
|
if scaleFilter != "" {
|
||||||
vf = append(vf, scaleFilter)
|
vf = append(vf, scaleFilter)
|
||||||
|
|
@ -2925,11 +2950,12 @@ func runGUI() {
|
||||||
AspectHandling: "Auto",
|
AspectHandling: "Auto",
|
||||||
AspectUserSet: false,
|
AspectUserSet: false,
|
||||||
},
|
},
|
||||||
player: player.New(),
|
mergeChapters: true,
|
||||||
playerVolume: 100,
|
player: player.New(),
|
||||||
lastVolume: 100,
|
playerVolume: 100,
|
||||||
playerMuted: false,
|
lastVolume: 100,
|
||||||
playerPaused: true,
|
playerMuted: false,
|
||||||
|
playerPaused: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg, err := loadPersistedConvertConfig(); err == nil {
|
if cfg, err := loadPersistedConvertConfig(); err == nil {
|
||||||
|
|
@ -3727,7 +3753,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
// Simple resolution selector (separate widget to avoid double-parent issues)
|
// Simple resolution selector (separate widget to avoid double-parent issues)
|
||||||
resolutionSelectSimple := widget.NewSelect([]string{
|
resolutionSelectSimple := widget.NewSelect([]string{
|
||||||
"Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K",
|
"Source", "360p", "480p", "540p", "720p", "1080p", "1440p", "4K",
|
||||||
"NTSC (720×480)", "PAL (720×576)",
|
"NTSC (720×480)", "PAL (720×540)", "PAL (720×576)",
|
||||||
}, func(value string) {
|
}, func(value string) {
|
||||||
state.convert.TargetResolution = value
|
state.convert.TargetResolution = value
|
||||||
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
logging.Debug(logging.CatUI, "target resolution set to %s (simple)", value)
|
||||||
|
|
@ -3910,7 +3936,7 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
updateEncodingControls()
|
updateEncodingControls()
|
||||||
|
|
||||||
// Target Resolution (advanced)
|
// Target Resolution (advanced)
|
||||||
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×576)"}, func(value string) {
|
resolutionSelect := widget.NewSelect([]string{"Source", "720p", "1080p", "1440p", "4K", "NTSC (720×480)", "PAL (720×540)", "PAL (720×576)"}, func(value string) {
|
||||||
state.convert.TargetResolution = value
|
state.convert.TargetResolution = value
|
||||||
logging.Debug(logging.CatUI, "target resolution set to %s", value)
|
logging.Debug(logging.CatUI, "target resolution set to %s", value)
|
||||||
})
|
})
|
||||||
|
|
@ -4051,6 +4077,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
targetFileSizeEntry.Enable()
|
targetFileSizeEntry.Enable()
|
||||||
targetFileSizeSelect.Enable()
|
targetFileSizeSelect.Enable()
|
||||||
crfEntry.Enable()
|
crfEntry.Enable()
|
||||||
|
bitratePresetSelect.Show()
|
||||||
|
simpleBitrateSelect.Show()
|
||||||
|
targetFileSizeEntry.Show()
|
||||||
|
targetFileSizeSelect.Show()
|
||||||
|
crfEntry.Show()
|
||||||
|
|
||||||
isDVD := state.convert.SelectedFormat.Ext == ".mpg"
|
isDVD := state.convert.SelectedFormat.Ext == ".mpg"
|
||||||
if isDVD {
|
if isDVD {
|
||||||
|
|
@ -4065,20 +4096,20 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
)
|
)
|
||||||
|
|
||||||
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
|
if strings.Contains(state.convert.SelectedFormat.Label, "NTSC") {
|
||||||
dvdNotes = "NTSC DVD: 720×480 @ 29.97fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 6000k default, 9000k max PS2-safe)"
|
dvdNotes = "NTSC DVD: 720×480 @ 29.97fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 8000k, 9000k max PS2-safe)"
|
||||||
targetRes = "NTSC (720×480)"
|
targetRes = "NTSC (720×480)"
|
||||||
targetFPS = "29.97"
|
targetFPS = "29.97"
|
||||||
dvdBitrate = "6000k"
|
dvdBitrate = "8000k"
|
||||||
} else if strings.Contains(state.convert.SelectedFormat.Label, "PAL") {
|
} else if strings.Contains(state.convert.SelectedFormat.Label, "PAL") {
|
||||||
dvdNotes = "PAL DVD: 720×576 @ 25fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 8000k default, 9500k max)"
|
dvdNotes = "PAL DVD: 720×540 @ 25fps, MPEG-2 Video, AC-3 Stereo 48kHz (bitrate 8000k default, 9500k max)"
|
||||||
targetRes = "PAL (720×576)"
|
targetRes = "PAL (720×540)"
|
||||||
targetFPS = "25"
|
targetFPS = "25"
|
||||||
dvdBitrate = "8000k"
|
dvdBitrate = "8000k"
|
||||||
} else {
|
} else {
|
||||||
dvdNotes = "DVD format selected"
|
dvdNotes = "DVD format selected"
|
||||||
targetRes = "NTSC (720×480)"
|
targetRes = "NTSC (720×480)"
|
||||||
targetFPS = "29.97"
|
targetFPS = "29.97"
|
||||||
dvdBitrate = "6000k"
|
dvdBitrate = "8000k"
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(strings.ToLower(state.convert.SelectedFormat.Label), "4:3") {
|
if strings.Contains(strings.ToLower(state.convert.SelectedFormat.Label), "4:3") {
|
||||||
|
|
@ -4133,9 +4164,22 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
targetFileSizeSelect.Disable()
|
targetFileSizeSelect.Disable()
|
||||||
crfEntry.Disable()
|
crfEntry.Disable()
|
||||||
|
|
||||||
|
// Hide bitrate/target-size fields to declutter in locked DVD mode
|
||||||
|
bitratePresetSelect.Hide()
|
||||||
|
simpleBitrateSelect.Hide()
|
||||||
|
targetFileSizeEntry.Hide()
|
||||||
|
targetFileSizeSelect.Hide()
|
||||||
|
crfEntry.Hide()
|
||||||
|
|
||||||
dvdInfoLabel.SetText(fmt.Sprintf("%s\nLocked: resolution, frame rate, aspect, codec, pixel format, bitrate, and GPU toggles for DVD compliance.", dvdNotes))
|
dvdInfoLabel.SetText(fmt.Sprintf("%s\nLocked: resolution, frame rate, aspect, codec, pixel format, bitrate, and GPU toggles for DVD compliance.", dvdNotes))
|
||||||
} else {
|
} else {
|
||||||
dvdAspectBox.Hide()
|
dvdAspectBox.Hide()
|
||||||
|
// Re-show hidden controls
|
||||||
|
bitratePresetSelect.Show()
|
||||||
|
simpleBitrateSelect.Show()
|
||||||
|
targetFileSizeEntry.Show()
|
||||||
|
targetFileSizeSelect.Show()
|
||||||
|
crfEntry.Show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updateDVDOptions()
|
updateDVDOptions()
|
||||||
|
|
@ -4326,7 +4370,11 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
src := state.source
|
src := state.source
|
||||||
outName := fmt.Sprintf("%s-snippet-%d.mp4", strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName)), time.Now().Unix())
|
ext := state.convert.SelectedFormat.Ext
|
||||||
|
if ext == "" {
|
||||||
|
ext = ".mp4"
|
||||||
|
}
|
||||||
|
outName := fmt.Sprintf("%s-snippet-%d%s", strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName)), time.Now().Unix(), ext)
|
||||||
outPath := filepath.Join(filepath.Dir(src.Path), outName)
|
outPath := filepath.Join(filepath.Dir(src.Path), outName)
|
||||||
|
|
||||||
cfgBytes, _ := json.Marshal(state.convert)
|
cfgBytes, _ := json.Marshal(state.convert)
|
||||||
|
|
@ -6347,6 +6395,15 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
// DVD presets: enforce compliant codecs and audio settings
|
// DVD presets: enforce compliant codecs and audio settings
|
||||||
// Note: We do NOT force resolution - user can choose Source or specific resolution
|
// Note: We do NOT force resolution - user can choose Source or specific resolution
|
||||||
if isDVD {
|
if isDVD {
|
||||||
|
if strings.Contains(cfg.SelectedFormat.Label, "PAL") {
|
||||||
|
cfg.TargetResolution = "PAL (720×540)"
|
||||||
|
cfg.FrameRate = "25"
|
||||||
|
} else {
|
||||||
|
cfg.TargetResolution = "NTSC (720×480)"
|
||||||
|
cfg.FrameRate = "29.97"
|
||||||
|
}
|
||||||
|
cfg.VideoBitrate = "8000k"
|
||||||
|
cfg.BitrateMode = "CBR"
|
||||||
if strings.Contains(cfg.SelectedFormat.Label, "PAL") {
|
if strings.Contains(cfg.SelectedFormat.Label, "PAL") {
|
||||||
// Only set frame rate if not already specified
|
// Only set frame rate if not already specified
|
||||||
if cfg.FrameRate == "" || cfg.FrameRate == "Source" {
|
if cfg.FrameRate == "" || cfg.FrameRate == "Source" {
|
||||||
|
|
@ -6477,6 +6534,12 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
scaleFilter = "scale=-2:2160"
|
scaleFilter = "scale=-2:2160"
|
||||||
case "8K":
|
case "8K":
|
||||||
scaleFilter = "scale=-2:4320"
|
scaleFilter = "scale=-2:4320"
|
||||||
|
case "NTSC (720×480)":
|
||||||
|
scaleFilter = "scale=720:480"
|
||||||
|
case "PAL (720×540)":
|
||||||
|
scaleFilter = "scale=720:540"
|
||||||
|
case "PAL (720×576)":
|
||||||
|
scaleFilter = "scale=720:576"
|
||||||
}
|
}
|
||||||
if scaleFilter != "" {
|
if scaleFilter != "" {
|
||||||
vf = append(vf, scaleFilter)
|
vf = append(vf, scaleFilter)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user