Compare commits
2 Commits
e76eeba60e
...
1f9df596bc
| Author | SHA1 | Date | |
|---|---|---|---|
| 1f9df596bc | |||
| b934797832 |
222
main.go
222
main.go
|
|
@ -2173,11 +2173,11 @@ func (s *appState) showMergeView() {
|
|||
|
||||
formatMap := map[string]string{
|
||||
"Fast Merge (No Re-encoding)": "mkv-copy",
|
||||
"Lossless MKV (Best Quality)": "mkv-lossless",
|
||||
"High Quality MP4 (H.264)": "mp4-h264",
|
||||
"High Quality MP4 (H.265)": "mp4-h265",
|
||||
"DVD Format": "dvd",
|
||||
"Blu-ray Format": "bd-h264",
|
||||
"Lossless MKV (Best Quality)": "mkv-lossless",
|
||||
"High Quality MP4 (H.264)": "mp4-h264",
|
||||
"High Quality MP4 (H.265)": "mp4-h265",
|
||||
"DVD Format": "dvd",
|
||||
"Blu-ray Format": "bd-h264",
|
||||
}
|
||||
// Maintain order for dropdown
|
||||
formatKeys := []string{
|
||||
|
|
@ -2591,10 +2591,10 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
|||
"-r", "30000/1001",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-aspect", dvdAspect,
|
||||
"-b:v", "5000k", // DVD video bitrate
|
||||
"-maxrate", "8000k", // DVD max bitrate
|
||||
"-b:v", "5000k", // DVD video bitrate
|
||||
"-maxrate", "8000k", // DVD max bitrate
|
||||
"-bufsize", "1835008", // DVD buffer size
|
||||
"-f", "dvd", // DVD format
|
||||
"-f", "dvd", // DVD format
|
||||
)
|
||||
} else {
|
||||
args = append(args,
|
||||
|
|
@ -2602,10 +2602,10 @@ func (s *appState) executeMergeJob(ctx context.Context, job *queue.Job, progress
|
|||
"-r", "25",
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-aspect", dvdAspect,
|
||||
"-b:v", "5000k", // DVD video bitrate
|
||||
"-maxrate", "8000k", // DVD max bitrate
|
||||
"-b:v", "5000k", // DVD video bitrate
|
||||
"-maxrate", "8000k", // DVD max bitrate
|
||||
"-bufsize", "1835008", // DVD buffer size
|
||||
"-f", "dvd", // DVD format
|
||||
"-f", "dvd", // DVD format
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -3535,7 +3535,7 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
|||
|
||||
// Apply encoder preset if supported codec
|
||||
if strings.Contains(strings.ToLower(videoCodec), "264") ||
|
||||
strings.Contains(strings.ToLower(videoCodec), "265") {
|
||||
strings.Contains(strings.ToLower(videoCodec), "265") {
|
||||
if conv.EncoderPreset != "" {
|
||||
args = append(args, "-preset", conv.EncoderPreset)
|
||||
} else {
|
||||
|
|
@ -3556,7 +3556,7 @@ func (s *appState) executeSnippetJob(ctx context.Context, job *queue.Job, progre
|
|||
|
||||
args = append(args, "-c:a", audioCodec)
|
||||
if strings.Contains(strings.ToLower(audioCodec), "aac") ||
|
||||
strings.Contains(strings.ToLower(audioCodec), "mp3") {
|
||||
strings.Contains(strings.ToLower(audioCodec), "mp3") {
|
||||
if conv.AudioBitrate != "" {
|
||||
args = append(args, "-b:a", conv.AudioBitrate)
|
||||
} else {
|
||||
|
|
@ -3694,6 +3694,10 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
method := cfg["method"].(string)
|
||||
targetWidth := int(cfg["targetWidth"].(float64))
|
||||
targetHeight := int(cfg["targetHeight"].(float64))
|
||||
preserveAR := true
|
||||
if v, ok := cfg["preserveAR"].(bool); ok {
|
||||
preserveAR = v
|
||||
}
|
||||
// useAI := cfg["useAI"].(bool) // TODO: Implement AI upscaling in future
|
||||
applyFilters := cfg["applyFilters"].(bool)
|
||||
|
||||
|
|
@ -3715,8 +3719,8 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
}
|
||||
}
|
||||
|
||||
// Add scale filter
|
||||
scaleFilter := buildUpscaleFilter(targetWidth, targetHeight, method)
|
||||
// Add scale filter (preserve aspect by default)
|
||||
scaleFilter := buildUpscaleFilter(targetWidth, targetHeight, method, preserveAR)
|
||||
filters = append(filters, scaleFilter)
|
||||
|
||||
// Combine filters
|
||||
|
|
@ -3737,12 +3741,13 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
args = append(args, "-vf", vfilter)
|
||||
}
|
||||
|
||||
// Use same video codec as source, but with high quality settings
|
||||
// Use lossless MKV by default for upscales; copy audio
|
||||
args = append(args,
|
||||
"-c:v", "libx264",
|
||||
"-preset", "slow",
|
||||
"-crf", "18",
|
||||
"-c:a", "copy", // Copy audio without re-encoding
|
||||
"-crf", "0", // lossless
|
||||
"-pix_fmt", "yuv420p",
|
||||
"-c:a", "copy",
|
||||
outputPath,
|
||||
)
|
||||
|
||||
|
|
@ -3784,7 +3789,7 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre
|
|||
var h, m int
|
||||
var s float64
|
||||
if _, err := fmt.Sscanf(timeStr, "%d:%d:%f", &h, &m, &s); err == nil {
|
||||
currentTime := float64(h*3600 + m*60) + s
|
||||
currentTime := float64(h*3600+m*60) + s
|
||||
progress := currentTime / duration
|
||||
if progress > 1.0 {
|
||||
progress = 1.0
|
||||
|
|
@ -7397,6 +7402,84 @@ func (s *appState) handleDrop(pos fyne.Position, items []fyne.URI) {
|
|||
return
|
||||
}
|
||||
|
||||
// If in filters module, handle single video file
|
||||
if s.active == "filters" {
|
||||
var videoPaths []string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() != "file" {
|
||||
continue
|
||||
}
|
||||
path := uri.Path()
|
||||
if s.isVideoFile(path) {
|
||||
videoPaths = append(videoPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(videoPaths) == 0 {
|
||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
||||
dialog.ShowInformation("Filters", "No video files found in dropped items.", s.window)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
src, err := probeVideo(videoPaths[0])
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatModule, "failed to load video for filters: %v", err)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.filtersFile = src
|
||||
s.showFiltersView()
|
||||
logging.Debug(logging.CatModule, "loaded video into filters module")
|
||||
}, false)
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If in upscale module, handle single video file
|
||||
if s.active == "upscale" {
|
||||
var videoPaths []string
|
||||
for _, uri := range items {
|
||||
if uri.Scheme() != "file" {
|
||||
continue
|
||||
}
|
||||
path := uri.Path()
|
||||
if s.isVideoFile(path) {
|
||||
videoPaths = append(videoPaths, path)
|
||||
}
|
||||
}
|
||||
|
||||
if len(videoPaths) == 0 {
|
||||
logging.Debug(logging.CatUI, "no valid video files in dropped items")
|
||||
dialog.ShowInformation("Upscale", "No video files found in dropped items.", s.window)
|
||||
return
|
||||
}
|
||||
|
||||
go func() {
|
||||
src, err := probeVideo(videoPaths[0])
|
||||
if err != nil {
|
||||
logging.Debug(logging.CatModule, "failed to load video for upscale: %v", err)
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
dialog.ShowError(fmt.Errorf("failed to load video: %w", err), s.window)
|
||||
}, false)
|
||||
return
|
||||
}
|
||||
|
||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||
s.upscaleFile = src
|
||||
s.showUpscaleView()
|
||||
logging.Debug(logging.CatModule, "loaded video into upscale module")
|
||||
}, false)
|
||||
}()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// If in merge module, handle multiple video files
|
||||
if s.active == "merge" {
|
||||
// Collect all video files from the dropped items
|
||||
|
|
@ -10688,11 +10771,11 @@ func buildFiltersView(state *appState) fyne.CanvasObject {
|
|||
|
||||
// Initialize state defaults
|
||||
if state.filterBrightness == 0 && state.filterContrast == 0 && state.filterSaturation == 0 {
|
||||
state.filterBrightness = 0.0 // -1.0 to 1.0
|
||||
state.filterContrast = 1.0 // 0.0 to 3.0
|
||||
state.filterSaturation = 1.0 // 0.0 to 3.0
|
||||
state.filterSharpness = 0.0 // 0.0 to 5.0
|
||||
state.filterDenoise = 0.0 // 0.0 to 10.0
|
||||
state.filterBrightness = 0.0 // -1.0 to 1.0
|
||||
state.filterContrast = 1.0 // 0.0 to 3.0
|
||||
state.filterSaturation = 1.0 // 0.0 to 3.0
|
||||
state.filterSharpness = 0.0 // 0.0 to 5.0
|
||||
state.filterDenoise = 0.0 // 0.0 to 10.0
|
||||
}
|
||||
|
||||
// File label
|
||||
|
|
@ -10860,7 +10943,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
state.upscaleMethod = "lanczos" // Best general-purpose traditional method
|
||||
}
|
||||
if state.upscaleTargetRes == "" {
|
||||
state.upscaleTargetRes = "1080p"
|
||||
state.upscaleTargetRes = "Match Source"
|
||||
}
|
||||
if state.upscaleAIModel == "" {
|
||||
state.upscaleAIModel = "realesrgan" // General purpose AI model
|
||||
|
|
@ -10954,6 +11037,9 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
// Resolution Selection Section
|
||||
resLabel := widget.NewLabel(fmt.Sprintf("Target: %s", state.upscaleTargetRes))
|
||||
resSelect := widget.NewSelect([]string{
|
||||
"Match Source",
|
||||
"2X (relative)",
|
||||
"4X (relative)",
|
||||
"720p (1280x720)",
|
||||
"1080p (1920x1080)",
|
||||
"1440p (2560x1440)",
|
||||
|
|
@ -11035,8 +11121,8 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
return nil, fmt.Errorf("no video loaded")
|
||||
}
|
||||
|
||||
// Parse target resolution
|
||||
targetWidth, targetHeight, err := parseResolutionPreset(state.upscaleTargetRes)
|
||||
// Parse target resolution (preserve aspect by default)
|
||||
targetWidth, targetHeight, preserveAspect, err := parseResolutionPreset(state.upscaleTargetRes, state.upscaleFile.Width, state.upscaleFile.Height)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid resolution: %w", err)
|
||||
}
|
||||
|
|
@ -11044,8 +11130,12 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
// Build output path
|
||||
videoDir := filepath.Dir(state.upscaleFile.Path)
|
||||
videoBaseName := strings.TrimSuffix(filepath.Base(state.upscaleFile.Path), filepath.Ext(state.upscaleFile.Path))
|
||||
outputPath := filepath.Join(videoDir, fmt.Sprintf("%s_upscaled_%s_%s.mp4",
|
||||
videoBaseName, state.upscaleTargetRes[:strings.Index(state.upscaleTargetRes, " ")], state.upscaleMethod))
|
||||
slug := sanitizeForPath(state.upscaleTargetRes)
|
||||
if slug == "" {
|
||||
slug = "source"
|
||||
}
|
||||
outputPath := filepath.Join(videoDir, fmt.Sprintf("%s_upscaled_%s_%s.mkv",
|
||||
videoBaseName, slug, state.upscaleMethod))
|
||||
|
||||
// Build description
|
||||
description := fmt.Sprintf("Upscale to %s using %s", state.upscaleTargetRes, state.upscaleMethod)
|
||||
|
|
@ -11063,6 +11153,7 @@ func buildUpscaleView(state *appState) fyne.CanvasObject {
|
|||
"method": state.upscaleMethod,
|
||||
"targetWidth": float64(targetWidth),
|
||||
"targetHeight": float64(targetHeight),
|
||||
"preserveAR": preserveAspect,
|
||||
"useAI": state.upscaleAIEnabled && state.upscaleAIAvailable,
|
||||
"aiModel": state.upscaleAIModel,
|
||||
"applyFilters": state.upscaleApplyFilters,
|
||||
|
|
@ -11159,36 +11250,69 @@ func checkAIUpscaleAvailable() bool {
|
|||
return false
|
||||
}
|
||||
|
||||
// parseResolutionPreset parses resolution preset strings like "1080p (1920x1080)" to width and height
|
||||
func parseResolutionPreset(preset string) (width, height int, err error) {
|
||||
// Extract dimensions from preset string
|
||||
// Format: "1080p (1920x1080)" or "4K (3840x2160)"
|
||||
// parseResolutionPreset parses resolution preset strings and returns target dimensions and whether to preserve aspect.
|
||||
// Special presets like "Match Source" and relative (2X/4X) use source dimensions to preserve AR.
|
||||
func parseResolutionPreset(preset string, srcW, srcH int) (width, height int, preserveAspect bool, err error) {
|
||||
// Default: preserve aspect
|
||||
preserveAspect = true
|
||||
|
||||
// Sanitize source
|
||||
if srcW < 1 || srcH < 1 {
|
||||
srcW, srcH = 1920, 1080 // fallback to avoid zero division
|
||||
}
|
||||
|
||||
switch preset {
|
||||
case "", "Match Source":
|
||||
return srcW, srcH, true, nil
|
||||
case "2X (relative)":
|
||||
return srcW * 2, srcH * 2, true, nil
|
||||
case "4X (relative)":
|
||||
return srcW * 4, srcH * 4, true, nil
|
||||
}
|
||||
|
||||
presetMap := map[string][2]int{
|
||||
"720p (1280x720)": {1280, 720},
|
||||
"1080p (1920x1080)": {1920, 1080},
|
||||
"1440p (2560x1440)": {2560, 1440},
|
||||
"4K (3840x2160)": {3840, 2160},
|
||||
"8K (7680x4320)": {7680, 4320},
|
||||
"720p": {1280, 720},
|
||||
"1080p": {1920, 1080},
|
||||
"1440p": {2560, 1440},
|
||||
"4K": {3840, 2160},
|
||||
"8K": {7680, 4320},
|
||||
"720p (1280x720)": {1280, 720},
|
||||
"1080p (1920x1080)": {1920, 1080},
|
||||
"1440p (2560x1440)": {2560, 1440},
|
||||
"4K (3840x2160)": {3840, 2160},
|
||||
"8K (7680x4320)": {7680, 4320},
|
||||
"720p": {1280, 720},
|
||||
"1080p": {1920, 1080},
|
||||
"1440p": {2560, 1440},
|
||||
"4K": {3840, 2160},
|
||||
"8K": {7680, 4320},
|
||||
}
|
||||
|
||||
if dims, ok := presetMap[preset]; ok {
|
||||
return dims[0], dims[1], nil
|
||||
// Keep aspect by default: use target height and let FFmpeg derive width
|
||||
return dims[0], dims[1], true, nil
|
||||
}
|
||||
|
||||
return 0, 0, fmt.Errorf("unknown resolution preset: %s", preset)
|
||||
return 0, 0, true, fmt.Errorf("unknown resolution preset: %s", preset)
|
||||
}
|
||||
|
||||
// buildUpscaleFilter builds the FFmpeg scale filter string with the selected method
|
||||
func buildUpscaleFilter(targetWidth, targetHeight int, method string) string {
|
||||
// Build scale filter with method (flags parameter)
|
||||
// Format: scale=width:height:flags=method
|
||||
return fmt.Sprintf("scale=%d:%d:flags=%s", targetWidth, targetHeight, method)
|
||||
func buildUpscaleFilter(targetWidth, targetHeight int, method string, preserveAspect bool) string {
|
||||
// Ensure even dimensions for encoders
|
||||
makeEven := func(v int) int {
|
||||
if v%2 != 0 {
|
||||
return v + 1
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
h := makeEven(targetHeight)
|
||||
w := targetWidth
|
||||
if preserveAspect || w <= 0 {
|
||||
w = -2 // FFmpeg will derive width from height while preserving AR
|
||||
}
|
||||
return fmt.Sprintf("scale=%d:%d:flags=%s", w, h, method)
|
||||
}
|
||||
|
||||
// sanitizeForPath creates a simple slug for filenames from user-visible labels
|
||||
func sanitizeForPath(label string) string {
|
||||
r := strings.NewReplacer(" ", "", "(", "", ")", "", "×", "x", "/", "-", "\\", "-", ":", "-", ",", "", ".", "", "_", "")
|
||||
return strings.ToLower(r.Replace(label))
|
||||
}
|
||||
|
||||
// buildCompareFullscreenView creates fullscreen side-by-side comparison with synchronized controls
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user