Compare commits

..

No commits in common. "fefe3ddd500e3050fec2152e542c888bbfa004a3" and "f62b64b0d507ec25be82aa1e731d951e26430855" have entirely different histories.

221
main.go
View File

@ -655,8 +655,7 @@ type appState struct {
upscaleFilterChain []string // Transferred filters from Filters module
// Snippet settings
snippetLength int // Length of snippet in seconds (default: 20)
snippetSourceFormat bool // true = source format, false = conversion format (default: true)
snippetLength int // Length of snippet in seconds (default: 20)
// Interlacing detection state
interlaceResult *interlace.DetectionResult
@ -3340,12 +3339,6 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
snippetLength = int(lengthVal)
}
// Get snippet mode, default to source format (true)
useSourceFormat := true
if modeVal, ok := cfg["useSourceFormat"].(bool); ok {
useSourceFormat = modeVal
}
// Probe video to get duration
src, err := probeVideo(inputPath)
if err != nil {
@ -3361,118 +3354,20 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
progressCallback(0)
}
var args []string
if useSourceFormat {
// Source format mode: Use stream copy for clean extraction
// Note: This uses keyframe cutting, so duration may not be frame-perfect
args = []string{
"-y",
"-hide_banner",
"-loglevel", "error",
"-ss", start,
"-i", inputPath,
"-t", fmt.Sprintf("%d", snippetLength),
"-c", "copy", // Stream copy - no re-encoding
"-map", "0", // Include all streams
outputPath,
}
} else {
// Conversion format mode: Use configured conversion settings
// This allows previewing what the final converted output will look like
conv := s.convert
args = []string{
"-y",
"-hide_banner",
"-loglevel", "error",
"-ss", start,
"-i", inputPath,
"-t", fmt.Sprintf("%d", snippetLength),
}
// Apply video codec settings
videoCodec := strings.ToLower(conv.VideoCodec)
switch videoCodec {
case "h.264", "":
args = append(args, "-c:v", "libx264")
if conv.EncoderPreset != "" {
args = append(args, "-preset", conv.EncoderPreset)
} else {
args = append(args, "-preset", "medium")
}
if conv.CRF != "" {
args = append(args, "-crf", conv.CRF)
} else {
args = append(args, "-crf", "23")
}
case "h.265":
args = append(args, "-c:v", "libx265")
if conv.EncoderPreset != "" {
args = append(args, "-preset", conv.EncoderPreset)
} else {
args = append(args, "-preset", "medium")
}
if conv.CRF != "" {
args = append(args, "-crf", conv.CRF)
} else {
args = append(args, "-crf", "28")
}
case "vp9":
args = append(args, "-c:v", "libvpx-vp9")
if conv.CRF != "" {
args = append(args, "-crf", conv.CRF)
} else {
args = append(args, "-crf", "31")
}
case "av1":
args = append(args, "-c:v", "libsvtav1")
if conv.CRF != "" {
args = append(args, "-crf", conv.CRF)
} else {
args = append(args, "-crf", "35")
}
case "copy":
args = append(args, "-c:v", "copy")
default:
// Fallback to h264
args = append(args, "-c:v", "libx264", "-preset", "medium", "-crf", "23")
}
// Apply audio codec settings
audioCodec := strings.ToLower(conv.AudioCodec)
switch audioCodec {
case "aac", "":
args = append(args, "-c:a", "aac")
if conv.AudioBitrate != "" {
args = append(args, "-b:a", conv.AudioBitrate)
} else {
args = append(args, "-b:a", "192k")
}
case "opus":
args = append(args, "-c:a", "libopus")
if conv.AudioBitrate != "" {
args = append(args, "-b:a", conv.AudioBitrate)
} else {
args = append(args, "-b:a", "128k")
}
case "mp3":
args = append(args, "-c:a", "libmp3lame")
if conv.AudioBitrate != "" {
args = append(args, "-b:a", conv.AudioBitrate)
} else {
args = append(args, "-b:a", "192k")
}
case "flac":
args = append(args, "-c:a", "flac")
case "copy":
args = append(args, "-c:a", "copy")
default:
// Fallback to AAC
args = append(args, "-c:a", "aac", "-b:a", "192k")
}
args = append(args, outputPath)
// Re-encode for precise duration control (stream copy can only cut at keyframes)
args := []string{
"-y",
"-hide_banner",
"-loglevel", "error",
"-ss", start,
"-i", inputPath,
"-t", fmt.Sprintf("%d", snippetLength),
"-c:v", "libx264", // Re-encode video for frame-accurate cutting
"-preset", "ultrafast", // Fast encoding for snippets
"-crf", "18", // High quality
"-c:a", "copy", // Copy audio without re-encoding
"-map", "0", // Include all streams
outputPath,
}
logFile, logPath, _ := createConversionLog(inputPath, outputPath, args)
@ -5286,14 +5181,10 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
optionsRect.StrokeWidth = 1
optionsPanel := container.NewMax(optionsRect, container.NewPadded(tabs))
// Initialize snippet settings defaults
// Initialize snippet length default
if state.snippetLength == 0 {
state.snippetLength = 20 // Default to 20 seconds
}
// Default to source format if not set
if !state.snippetSourceFormat {
state.snippetSourceFormat = true
}
// Snippet length configuration
snippetLengthLabel := widget.NewLabel(fmt.Sprintf("Snippet Length: %d seconds", state.snippetLength))
@ -5305,22 +5196,9 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
snippetLengthLabel.SetText(fmt.Sprintf("Snippet Length: %d seconds", state.snippetLength))
}
// Snippet output mode
snippetModeLabel := widget.NewLabel("Snippet Output:")
snippetModeCheck := widget.NewCheck("Snippet to Default Format (preserves source quality)", func(checked bool) {
state.snippetSourceFormat = checked
})
snippetModeCheck.SetChecked(state.snippetSourceFormat)
snippetModeHint := widget.NewLabel("Unchecked = Snippet to Output Format (uses conversion settings)")
snippetModeHint.TextStyle = fyne.TextStyle{Italic: true}
snippetConfigRow := container.NewVBox(
snippetLengthLabel,
snippetLengthSlider,
widget.NewSeparator(),
snippetModeLabel,
snippetModeCheck,
snippetModeHint,
)
snippetBtn := widget.NewButton("Generate Snippet", func() {
@ -5333,42 +5211,24 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
return
}
src := state.source
// Determine output extension based on mode
var ext string
if state.snippetSourceFormat {
// Source format: use source extension
ext = filepath.Ext(src.Path)
if ext == "" {
ext = ".mp4"
}
} else {
// Conversion format: use configured output format
ext = state.convert.SelectedFormat.Ext
if ext == "" {
ext = ".mp4"
}
// Use same extension as source file since we're using stream copy
ext := filepath.Ext(src.Path)
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)
modeDesc := "conversion settings"
if state.snippetSourceFormat {
modeDesc = "source format"
}
job := &queue.Job{
Type: queue.JobTypeSnippet,
Title: "Snippet: " + filepath.Base(src.Path),
Description: fmt.Sprintf("%ds snippet centred on midpoint (%s)", state.snippetLength, modeDesc),
Description: fmt.Sprintf("%ds snippet centred on midpoint (source settings)", state.snippetLength),
InputFile: src.Path,
OutputFile: outPath,
Config: map[string]interface{}{
"inputPath": src.Path,
"outputPath": outPath,
"snippetLength": float64(state.snippetLength),
"useSourceFormat": state.snippetSourceFormat,
"inputPath": src.Path,
"outputPath": outPath,
"snippetLength": float64(state.snippetLength),
},
}
state.jobQueue.Add(job)
@ -5394,46 +5254,29 @@ func buildConvertView(state *appState, src *videoSource) fyne.CanvasObject {
timestamp := time.Now().Unix()
jobsAdded := 0
modeDesc := "conversion settings"
if state.snippetSourceFormat {
modeDesc = "source format"
}
for _, src := range state.loadedVideos {
if src == nil {
continue
}
// Determine output extension based on mode
var ext string
if state.snippetSourceFormat {
// Source format: use source extension
ext = filepath.Ext(src.Path)
if ext == "" {
ext = ".mp4"
}
} else {
// Conversion format: use configured output format
ext = state.convert.SelectedFormat.Ext
if ext == "" {
ext = ".mp4"
}
// Use same extension as source file since we're using stream copy
ext := filepath.Ext(src.Path)
if ext == "" {
ext = ".mp4"
}
outName := fmt.Sprintf("%s-snippet-%d%s", strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName)), timestamp, ext)
outPath := filepath.Join(filepath.Dir(src.Path), outName)
job := &queue.Job{
Type: queue.JobTypeSnippet,
Title: "Snippet: " + filepath.Base(src.Path),
Description: fmt.Sprintf("%ds snippet centred on midpoint (%s)", state.snippetLength, modeDesc),
Description: fmt.Sprintf("%ds snippet centred on midpoint (source settings)", state.snippetLength),
InputFile: src.Path,
OutputFile: outPath,
Config: map[string]interface{}{
"inputPath": src.Path,
"outputPath": outPath,
"snippetLength": float64(state.snippetLength),
"useSourceFormat": state.snippetSourceFormat,
"inputPath": src.Path,
"outputPath": outPath,
"snippetLength": float64(state.snippetLength),
},
}
state.jobQueue.Add(job)