Add per-conversion logs and surface them in queue UI
This commit is contained in:
parent
f900f6804d
commit
8e601bc7d2
|
|
@ -44,6 +44,7 @@ type Job struct {
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
InputFile string `json:"input_file"`
|
InputFile string `json:"input_file"`
|
||||||
OutputFile string `json:"output_file"`
|
OutputFile string `json:"output_file"`
|
||||||
|
LogPath string `json:"log_path,omitempty"`
|
||||||
Config map[string]interface{} `json:"config"`
|
Config map[string]interface{} `json:"config"`
|
||||||
Progress float64 `json:"progress"`
|
Progress float64 `json:"progress"`
|
||||||
Error string `json:"error,omitempty"`
|
Error string `json:"error,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,7 @@ func BuildQueueView(
|
||||||
onClear func(),
|
onClear func(),
|
||||||
onClearAll func(),
|
onClearAll func(),
|
||||||
onCopyError func(string),
|
onCopyError func(string),
|
||||||
|
onViewLog func(string),
|
||||||
titleColor, bgColor, textColor color.Color,
|
titleColor, bgColor, textColor color.Color,
|
||||||
) (fyne.CanvasObject, *container.Scroll) {
|
) (fyne.CanvasObject, *container.Scroll) {
|
||||||
// Header
|
// Header
|
||||||
|
|
@ -73,7 +74,7 @@ func BuildQueueView(
|
||||||
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
jobItems = append(jobItems, container.NewCenter(emptyMsg))
|
||||||
} else {
|
} else {
|
||||||
for _, job := range jobs {
|
for _, job := range jobs {
|
||||||
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, bgColor, textColor))
|
jobItems = append(jobItems, buildJobItem(job, onPause, onResume, onCancel, onRemove, onMoveUp, onMoveDown, onCopyError, onViewLog, bgColor, textColor))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,6 +103,7 @@ func buildJobItem(
|
||||||
onMoveUp func(string),
|
onMoveUp func(string),
|
||||||
onMoveDown func(string),
|
onMoveDown func(string),
|
||||||
onCopyError func(string),
|
onCopyError func(string),
|
||||||
|
onViewLog func(string),
|
||||||
bgColor, textColor color.Color,
|
bgColor, textColor color.Color,
|
||||||
) fyne.CanvasObject {
|
) fyne.CanvasObject {
|
||||||
// Status color
|
// Status color
|
||||||
|
|
@ -165,6 +167,11 @@ func buildJobItem(
|
||||||
widget.NewButton("Copy Error", func() { onCopyError(job.ID) }),
|
widget.NewButton("Copy Error", func() { onCopyError(job.ID) }),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if job.LogPath != "" && onViewLog != nil {
|
||||||
|
buttons = append(buttons,
|
||||||
|
widget.NewButton("View Log", func() { onViewLog(job.ID) }),
|
||||||
|
)
|
||||||
|
}
|
||||||
buttons = append(buttons,
|
buttons = append(buttons,
|
||||||
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
widget.NewButton("Remove", func() { onRemove(job.ID) }),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
102
main.go
102
main.go
|
|
@ -60,6 +60,8 @@ var (
|
||||||
textColor = utils.MustHex("#E1EEFF")
|
textColor = utils.MustHex("#E1EEFF")
|
||||||
queueColor = utils.MustHex("#5961FF")
|
queueColor = utils.MustHex("#5961FF")
|
||||||
|
|
||||||
|
conversionLogSuffix = ".videotools.log"
|
||||||
|
|
||||||
modulesList = []Module{
|
modulesList = []Module{
|
||||||
{"convert", "Convert", utils.MustHex("#8B44FF"), modules.HandleConvert}, // Violet
|
{"convert", "Convert", utils.MustHex("#8B44FF"), modules.HandleConvert}, // Violet
|
||||||
{"merge", "Merge", utils.MustHex("#4488FF"), modules.HandleMerge}, // Blue
|
{"merge", "Merge", utils.MustHex("#4488FF"), modules.HandleMerge}, // Blue
|
||||||
|
|
@ -100,6 +102,29 @@ func resolveTargetAspect(val string, src *videoSource) float64 {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createConversionLog(inputPath, outputPath string, args []string) (*os.File, string, error) {
|
||||||
|
logPath := outputPath + conversionLogSuffix
|
||||||
|
if err := os.MkdirAll(filepath.Dir(logPath), 0o755); err != nil {
|
||||||
|
return nil, logPath, fmt.Errorf("create log dir: %w", err)
|
||||||
|
}
|
||||||
|
f, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return nil, logPath, err
|
||||||
|
}
|
||||||
|
header := fmt.Sprintf(`VideoTools Conversion Log
|
||||||
|
Started: %s
|
||||||
|
Input: %s
|
||||||
|
Output: %s
|
||||||
|
Command: ffmpeg %s
|
||||||
|
|
||||||
|
`, time.Now().Format(time.RFC3339), inputPath, outputPath, strings.Join(args, " "))
|
||||||
|
if _, err := f.WriteString(header); err != nil {
|
||||||
|
_ = f.Close()
|
||||||
|
return nil, logPath, err
|
||||||
|
}
|
||||||
|
return f, logPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
type formatOption struct {
|
type formatOption struct {
|
||||||
Label string
|
Label string
|
||||||
Ext string
|
Ext string
|
||||||
|
|
@ -612,6 +637,28 @@ func (s *appState) refreshQueueView() {
|
||||||
}
|
}
|
||||||
s.window.Clipboard().SetContent(text)
|
s.window.Clipboard().SetContent(text)
|
||||||
},
|
},
|
||||||
|
func(id string) { // onViewLog
|
||||||
|
job, err := s.jobQueue.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
logging.Debug(logging.CatSystem, "view log failed: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
path := strings.TrimSpace(job.LogPath)
|
||||||
|
if path == "" {
|
||||||
|
dialog.ShowInformation("No Log", "No log path recorded for this job.", s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
dialog.ShowError(fmt.Errorf("failed to read log: %w", err), s.window)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
text := widget.NewMultiLineEntry()
|
||||||
|
text.SetText(string(data))
|
||||||
|
text.Wrapping = fyne.TextWrapWord
|
||||||
|
text.Disable()
|
||||||
|
dialog.ShowCustom("Conversion Log", "Close", container.NewVScroll(text), s.window)
|
||||||
|
},
|
||||||
utils.MustHex("#4CE870"), // titleColor
|
utils.MustHex("#4CE870"), // titleColor
|
||||||
gridColor, // bgColor
|
gridColor, // bgColor
|
||||||
textColor, // textColor
|
textColor, // textColor
|
||||||
|
|
@ -1530,6 +1577,15 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
args = append(args, "-progress", "pipe:1", "-nostats")
|
args = append(args, "-progress", "pipe:1", "-nostats")
|
||||||
args = append(args, outputPath)
|
args = append(args, outputPath)
|
||||||
|
|
||||||
|
logFile, logPath, logErr := createConversionLog(inputPath, outputPath, args)
|
||||||
|
if logErr != nil {
|
||||||
|
logging.Debug(logging.CatFFMPEG, "conversion log open failed: %v", logErr)
|
||||||
|
} else {
|
||||||
|
job.LogPath = logPath
|
||||||
|
fmt.Fprintf(logFile, "Status: started\n\n")
|
||||||
|
defer logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
logging.Debug(logging.CatFFMPEG, "queue convert command: ffmpeg %s", strings.Join(args, " "))
|
logging.Debug(logging.CatFFMPEG, "queue convert command: ffmpeg %s", strings.Join(args, " "))
|
||||||
|
|
||||||
// Also print to stdout for debugging
|
// Also print to stdout for debugging
|
||||||
|
|
@ -1545,14 +1601,22 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
|
|
||||||
// Capture stderr for error messages
|
// Capture stderr for error messages
|
||||||
var stderrBuf strings.Builder
|
var stderrBuf strings.Builder
|
||||||
cmd.Stderr = &stderrBuf
|
if logFile != nil {
|
||||||
|
cmd.Stderr = io.MultiWriter(&stderrBuf, logFile)
|
||||||
|
} else {
|
||||||
|
cmd.Stderr = &stderrBuf
|
||||||
|
}
|
||||||
|
|
||||||
if err := cmd.Start(); err != nil {
|
if err := cmd.Start(); err != nil {
|
||||||
return fmt.Errorf("failed to start ffmpeg: %w", err)
|
return fmt.Errorf("failed to start ffmpeg: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse progress
|
// Parse progress
|
||||||
scanner := bufio.NewScanner(stdout)
|
stdoutReader := io.Reader(stdout)
|
||||||
|
if logFile != nil {
|
||||||
|
stdoutReader = io.TeeReader(stdout, logFile)
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
var duration float64
|
var duration float64
|
||||||
if d, ok := cfg["sourceDuration"].(float64); ok && d > 0 {
|
if d, ok := cfg["sourceDuration"].(float64); ok && d > 0 {
|
||||||
duration = d
|
duration = d
|
||||||
|
|
@ -1658,6 +1722,9 @@ func (s *appState) executeConvertJob(ctx context.Context, job *queue.Job, progre
|
||||||
return fmt.Errorf("%s", errorMsg)
|
return fmt.Errorf("%s", errorMsg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if logFile != nil {
|
||||||
|
fmt.Fprintf(logFile, "\nStatus: completed OK at %s\n", time.Now().Format(time.RFC3339))
|
||||||
|
}
|
||||||
logging.Debug(logging.CatFFMPEG, "queue conversion completed: %s", outputPath)
|
logging.Debug(logging.CatFFMPEG, "queue conversion completed: %s", outputPath)
|
||||||
|
|
||||||
// Auto-compare if enabled
|
// Auto-compare if enabled
|
||||||
|
|
@ -5381,6 +5448,13 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
s.convertProgress = 0
|
s.convertProgress = 0
|
||||||
s.convertActiveIn = src.Path
|
s.convertActiveIn = src.Path
|
||||||
s.convertActiveOut = outPath
|
s.convertActiveOut = outPath
|
||||||
|
logFile, logPath, logErr := createConversionLog(src.Path, outPath, args)
|
||||||
|
if logErr != nil {
|
||||||
|
logging.Debug(logging.CatFFMPEG, "conversion log open failed: %v", logErr)
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(logFile, "Status: started\n\n")
|
||||||
|
}
|
||||||
|
_ = logPath
|
||||||
setStatus("Preparing conversion…")
|
setStatus("Preparing conversion…")
|
||||||
// Widget states will be updated by the UI refresh ticker
|
// Widget states will be updated by the UI refresh ticker
|
||||||
|
|
||||||
|
|
@ -5391,6 +5465,9 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
setStatus("Running ffmpeg…")
|
setStatus("Running ffmpeg…")
|
||||||
}, false)
|
}, false)
|
||||||
|
if logFile != nil {
|
||||||
|
defer logFile.Close()
|
||||||
|
}
|
||||||
|
|
||||||
started := time.Now()
|
started := time.Now()
|
||||||
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...)
|
cmd := exec.CommandContext(ctx, platformConfig.FFmpegPath, args...)
|
||||||
|
|
@ -5407,11 +5484,19 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
cmd.Stderr = &stderr
|
if logFile != nil {
|
||||||
|
cmd.Stderr = io.MultiWriter(&stderr, logFile)
|
||||||
|
} else {
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
}
|
||||||
|
|
||||||
progressQuit := make(chan struct{})
|
progressQuit := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
scanner := bufio.NewScanner(stdout)
|
stdoutReader := io.Reader(stdout)
|
||||||
|
if logFile != nil {
|
||||||
|
stdoutReader = io.TeeReader(stdout, logFile)
|
||||||
|
}
|
||||||
|
scanner := bufio.NewScanner(stdoutReader)
|
||||||
var currentFPS float64
|
var currentFPS float64
|
||||||
for scanner.Scan() {
|
for scanner.Scan() {
|
||||||
select {
|
select {
|
||||||
|
|
@ -5510,6 +5595,9 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
if errors.Is(err, context.Canceled) || ctx.Err() != nil {
|
||||||
logging.Debug(logging.CatFFMPEG, "convert cancelled")
|
logging.Debug(logging.CatFFMPEG, "convert cancelled")
|
||||||
|
if logFile != nil {
|
||||||
|
fmt.Fprintf(logFile, "\nStatus: cancelled at %s\n", time.Now().Format(time.RFC3339))
|
||||||
|
}
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
s.convertBusy = false
|
s.convertBusy = false
|
||||||
s.convertActiveIn = ""
|
s.convertActiveIn = ""
|
||||||
|
|
@ -5522,6 +5610,9 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
}
|
}
|
||||||
stderrOutput := strings.TrimSpace(stderr.String())
|
stderrOutput := strings.TrimSpace(stderr.String())
|
||||||
logging.Debug(logging.CatFFMPEG, "convert failed: %v stderr=%s", err, stderrOutput)
|
logging.Debug(logging.CatFFMPEG, "convert failed: %v stderr=%s", err, stderrOutput)
|
||||||
|
if logFile != nil {
|
||||||
|
fmt.Fprintf(logFile, "\nStatus: failed at %s\nError: %v\nStderr:\n%s\n", time.Now().Format(time.RFC3339), err, stderrOutput)
|
||||||
|
}
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
errorExplanation := interpretFFmpegError(err)
|
errorExplanation := interpretFFmpegError(err)
|
||||||
var errorMsg error
|
var errorMsg error
|
||||||
|
|
@ -5560,6 +5651,9 @@ func (s *appState) startConvert(status *widget.Label, btn, cancelBtn *widget.But
|
||||||
s.convertCancel = nil
|
s.convertCancel = nil
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if logFile != nil {
|
||||||
|
fmt.Fprintf(logFile, "\nStatus: completed OK at %s\n", time.Now().Format(time.RFC3339))
|
||||||
|
}
|
||||||
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
fyne.CurrentApp().Driver().DoFromGoroutine(func() {
|
||||||
setStatus("Validating output…")
|
setStatus("Validating output…")
|
||||||
}, false)
|
}, false)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user