Add queue error copy, auto naming helper, and metadata templating

This commit is contained in:
Stu Leak 2025-12-07 12:03:21 -05:00
parent c908b22128
commit 53b1b839c5
3 changed files with 171 additions and 1 deletions

View File

@ -0,0 +1,83 @@
package metadata
import (
"regexp"
"strings"
"unicode"
)
var tokenPattern = regexp.MustCompile(`<([a-zA-Z0-9_-]+)>`)
// RenderTemplate applies a simple <token> template to the provided metadata map.
// It returns the rendered string and a boolean indicating whether any tokens were resolved.
func RenderTemplate(pattern string, meta map[string]string, fallback string) (string, bool) {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
return fallback, false
}
normalized := make(map[string]string, len(meta))
for k, v := range meta {
key := strings.ToLower(strings.TrimSpace(k))
if key == "" {
continue
}
val := sanitize(v)
if val != "" {
normalized[key] = val
}
}
resolved := false
rendered := tokenPattern.ReplaceAllStringFunc(pattern, func(tok string) string {
match := tokenPattern.FindStringSubmatch(tok)
if len(match) != 2 {
return ""
}
key := strings.ToLower(match[1])
if val := normalized[key]; val != "" {
resolved = true
return val
}
return ""
})
rendered = cleanup(rendered)
if rendered == "" {
return fallback, false
}
return rendered, resolved
}
func sanitize(value string) string {
value = strings.TrimSpace(value)
value = strings.Map(func(r rune) rune {
switch r {
case '<', '>', '"', '/', '\\', '|', '?', '*', ':':
return -1
}
if unicode.IsControl(r) {
return -1
}
return r
}, value)
// Collapse repeated whitespace
value = strings.Join(strings.Fields(value), " ")
return strings.Trim(value, " .-_")
}
func cleanup(s string) string {
// Remove leftover template brackets or duplicate separators.
s = strings.ReplaceAll(s, "<>", "")
for strings.Contains(s, " ") {
s = strings.ReplaceAll(s, " ", " ")
}
for strings.Contains(s, "__") {
s = strings.ReplaceAll(s, "__", "_")
}
for strings.Contains(s, "--") {
s = strings.ReplaceAll(s, "--", "-")
}
return strings.Trim(s, " .-_")
}

View File

@ -3,6 +3,7 @@ package ui
import (
"fmt"
"image/color"
"strings"
"time"
"fyne.io/fyne/v2"
@ -28,6 +29,7 @@ func BuildQueueView(
onStart func(),
onClear func(),
onClearAll func(),
onCopyError func(string),
titleColor, bgColor, textColor color.Color,
) (fyne.CanvasObject, *container.Scroll) {
// Header
@ -71,7 +73,7 @@ func BuildQueueView(
jobItems = append(jobItems, container.NewCenter(emptyMsg))
} else {
for _, job := range jobs {
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, bgColor, textColor))
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, bgColor, textColor))
}
}
@ -99,6 +101,7 @@ func buildJobItem(
onRemove func(string),
onMoveUp func(string),
onMoveDown func(string),
onCopyError func(string),
bgColor, textColor color.Color,
) fyne.CanvasObject {
// Status color
@ -157,6 +160,11 @@ func buildJobItem(
widget.NewButton("Cancel", func() { onCancel(job.ID) }),
)
case queue.JobStatusCompleted, queue.JobStatusFailed, queue.JobStatusCancelled:
if job.Status == queue.JobStatusFailed && strings.TrimSpace(job.Error) != "" && onCopyError != nil {
buttons = append(buttons,
widget.NewButton("Copy Error", func() { onCopyError(job.ID) }),
)
}
buttons = append(buttons,
widget.NewButton("Remove", func() { onRemove(job.ID) }),
)

79
naming_helpers.go Normal file
View File

@ -0,0 +1,79 @@
package main
import (
"fmt"
"path/filepath"
"strings"
"git.leaktechnologies.dev/stu/VideoTools/internal/metadata"
)
func defaultOutputBase(src *videoSource) string {
if src == nil {
return "converted"
}
base := strings.TrimSuffix(src.DisplayName, filepath.Ext(src.DisplayName))
return base + "-convert"
}
// resolveOutputBase returns the output base for a source.
// keepExisting preserves manual edits when auto-naming is disabled; it is ignored when auto-naming is on.
func (s *appState) resolveOutputBase(src *videoSource, keepExisting bool) string {
fallback := defaultOutputBase(src)
// Auto-naming overrides manual values.
if s.convert.UseAutoNaming && src != nil && strings.TrimSpace(s.convert.AutoNameTemplate) != "" {
if name, ok := metadata.RenderTemplate(s.convert.AutoNameTemplate, buildNamingMetadata(src), fallback); ok || name != "" {
return name
}
return fallback
}
if keepExisting {
if base := strings.TrimSpace(s.convert.OutputBase); base != "" {
return base
}
}
return fallback
}
func buildNamingMetadata(src *videoSource) map[string]string {
meta := map[string]string{}
if src == nil {
return meta
}
meta["filename"] = strings.TrimSuffix(filepath.Base(src.Path), filepath.Ext(src.Path))
meta["format"] = src.Format
meta["codec"] = src.VideoCodec
if src.Width > 0 && src.Height > 0 {
meta["width"] = fmt.Sprintf("%d", src.Width)
meta["height"] = fmt.Sprintf("%d", src.Height)
meta["resolution"] = fmt.Sprintf("%dx%d", src.Width, src.Height)
}
for k, v := range src.Metadata {
meta[k] = v
}
aliasMetadata(meta, "title", "title")
aliasMetadata(meta, "scene", "title", "comment", "description")
aliasMetadata(meta, "studio", "studio", "publisher", "label")
aliasMetadata(meta, "actress", "actress", "performer", "performers", "artist", "actors", "cast")
aliasMetadata(meta, "series", "series", "album")
aliasMetadata(meta, "date", "date", "year")
return meta
}
func aliasMetadata(meta map[string]string, target string, keys ...string) {
if meta[target] != "" {
return
}
for _, key := range keys {
if val := meta[strings.ToLower(key)]; strings.TrimSpace(val) != "" {
meta[target] = val
return
}
}
}