diff --git a/internal/queue/edit.go b/internal/queue/edit.go new file mode 100644 index 0000000..c9da804 --- /dev/null +++ b/internal/queue/edit.go @@ -0,0 +1,362 @@ +package queue + +import ( + "encoding/json" + "fmt" + "strings" + "time" +) + +// EditJobStatus represents the edit state of a job +type EditJobStatus string + +const ( + EditJobStatusOriginal EditJobStatus = "original" // Original job state + EditJobStatusModified EditJobStatus = "modified" // Job has been modified + EditJobStatusValidated EditJobStatus = "validated" // Job has been validated + EditJobStatusApplied EditJobStatus = "applied" // Changes have been applied +) + +// EditHistoryEntry tracks changes made to a job +type EditHistoryEntry struct { + Timestamp time.Time `json:"timestamp"` + OldCommand *FFmpegCommand `json:"old_command,omitempty"` + NewCommand *FFmpegCommand `json:"new_command"` + ChangeReason string `json:"change_reason"` + Applied bool `json:"applied"` +} + +// FFmpegCommand represents a structured FFmpeg command +type FFmpegCommand struct { + Executable string `json:"executable"` + Args []string `json:"args"` + InputFile string `json:"input_file"` + OutputFile string `json:"output_file"` + Options map[string]string `json:"options,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` +} + +// EditableJob extends Job with editing capabilities +type EditableJob struct { + *Job + EditStatus EditJobStatus `json:"edit_status"` + EditHistory []EditHistoryEntry `json:"edit_history"` + OriginalCommand *FFmpegCommand `json:"original_command"` + CurrentCommand *FFmpegCommand `json:"current_command"` +} + +// EditJobManager manages job editing operations +type EditJobManager interface { + // GetEditableJob returns an editable version of a job + GetEditableJob(id string) (*EditableJob, error) + + // UpdateJobCommand updates a job's FFmpeg command + UpdateJobCommand(id string, newCommand *FFmpegCommand, reason string) error + + // ValidateCommand validates an FFmpeg command + ValidateCommand(cmd *FFmpegCommand) error + + // GetEditHistory returns the edit history for a job + GetEditHistory(id string) ([]EditHistoryEntry, error) + + // ApplyEdit applies pending edits to a job + ApplyEdit(id string) error + + // ResetToOriginal resets a job to its original command + ResetToOriginal(id string) error + + // CreateEditableJob creates a new editable job + CreateEditableJob(job *Job, cmd *FFmpegCommand) (*EditableJob, error) +} + +// editJobManager implements EditJobManager +type editJobManager struct { + queue *Queue +} + +// NewEditJobManager creates a new edit job manager +func NewEditJobManager(queue *Queue) EditJobManager { + return &editJobManager{queue: queue} +} + +// GetEditableJob returns an editable version of a job +func (e *editJobManager) GetEditableJob(id string) (*EditableJob, error) { + job, err := e.queue.Get(id) + if err != nil { + return nil, err + } + + editable := &EditableJob{ + Job: job, + EditStatus: EditJobStatusOriginal, + EditHistory: make([]EditHistoryEntry, 0), + } + + // Extract current command from job config if available + if cmd, err := e.extractCommandFromJob(job); err == nil { + editable.OriginalCommand = cmd + editable.CurrentCommand = cmd + } + + return editable, nil +} + +// UpdateJobCommand updates a job's FFmpeg command +func (e *editJobManager) UpdateJobCommand(id string, newCommand *FFmpegCommand, reason string) error { + job, err := e.queue.Get(id) + if err != nil { + return err + } + + // Validate the new command + if err := e.ValidateCommand(newCommand); err != nil { + return fmt.Errorf("invalid command: %w", err) + } + + // Create history entry + oldCmd, _ := e.extractCommandFromJob(job) + history := EditHistoryEntry{ + Timestamp: time.Now(), + OldCommand: oldCmd, + NewCommand: newCommand, + ChangeReason: reason, + Applied: false, + } + + // Update job config with new command + if job.Config == nil { + job.Config = make(map[string]interface{}) + } + job.Config["ffmpeg_command"] = newCommand + + // Update job metadata + job.Config["last_edited"] = time.Now().Format(time.RFC3339) + job.Config["edit_reason"] = reason + + // Add to edit history + editHistory := []EditHistoryEntry{history} + if existingHistoryInterface, exists := job.Config["edit_history"]; exists { + if historyBytes, err := json.Marshal(existingHistoryInterface); err == nil { + var existingHistory []EditHistoryEntry + if err := json.Unmarshal(historyBytes, &existingHistory); err == nil { + editHistory = append(existingHistory, history) + } + } + } + job.Config["edit_history"] = editHistory + + return nil +} + +// ValidateCommand validates an FFmpeg command +func (e *editJobManager) ValidateCommand(cmd *FFmpegCommand) error { + if cmd == nil { + return fmt.Errorf("command cannot be nil") + } + + if cmd.Executable == "" { + return fmt.Errorf("executable cannot be empty") + } + + if len(cmd.Args) == 0 { + return fmt.Errorf("command arguments cannot be empty") + } + + // Basic validation for input/output files + if cmd.InputFile != "" && !strings.Contains(cmd.InputFile, "INPUT") { + // Check if input file path is valid (basic check) + if strings.HasPrefix(cmd.InputFile, "-") { + return fmt.Errorf("input file cannot start with '-'") + } + } + + if cmd.OutputFile != "" && !strings.Contains(cmd.OutputFile, "OUTPUT") { + // Check if output file path is valid (basic check) + if strings.HasPrefix(cmd.OutputFile, "-") { + return fmt.Errorf("output file cannot start with '-'") + } + } + + return nil +} + +// GetEditHistory returns the edit history for a job +func (e *editJobManager) GetEditHistory(id string) ([]EditHistoryEntry, error) { + job, err := e.queue.Get(id) + if err != nil { + return nil, err + } + + // Extract history from job config + if historyInterface, exists := job.Config["edit_history"]; exists { + if historyBytes, err := json.Marshal(historyInterface); err == nil { + var history []EditHistoryEntry + if err := json.Unmarshal(historyBytes, &history); err == nil { + return history, nil + } + } + } + + return make([]EditHistoryEntry, 0), nil +} + +// ApplyEdit applies pending edits to a job +func (e *editJobManager) ApplyEdit(id string) error { + job, err := e.queue.Get(id) + if err != nil { + return err + } + + // Mark edit as applied + if job.Config == nil { + job.Config = make(map[string]interface{}) + } + job.Config["edit_applied"] = time.Now().Format(time.RFC3339) + + return nil +} + +// ResetToOriginal resets a job to its original command +func (e *editJobManager) ResetToOriginal(id string) error { + job, err := e.queue.Get(id) + if err != nil { + return err + } + + // Get original command from job config + if originalInterface, exists := job.Config["original_command"]; exists { + if job.Config == nil { + job.Config = make(map[string]interface{}) + } + job.Config["ffmpeg_command"] = originalInterface + job.Config["reset_to_original"] = time.Now().Format(time.RFC3339) + } + + return nil +} + +// CreateEditableJob creates a new editable job +func (e *editJobManager) CreateEditableJob(job *Job, cmd *FFmpegCommand) (*EditableJob, error) { + if err := e.ValidateCommand(cmd); err != nil { + return nil, fmt.Errorf("invalid command: %w", err) + } + + editable := &EditableJob{ + Job: job, + EditStatus: EditJobStatusOriginal, + EditHistory: make([]EditHistoryEntry, 0), + OriginalCommand: cmd, + CurrentCommand: cmd, + } + + // Store command in job config + if job.Config == nil { + job.Config = make(map[string]interface{}) + } + job.Config["ffmpeg_command"] = cmd + job.Config["original_command"] = cmd + + return editable, nil +} + +// extractCommandFromJob extracts FFmpeg command from job config +func (e *editJobManager) extractCommandFromJob(job *Job) (*FFmpegCommand, error) { + if job.Config == nil { + return nil, fmt.Errorf("job has no config") + } + + if cmdInterface, exists := job.Config["ffmpeg_command"]; exists { + if cmdBytes, err := json.Marshal(cmdInterface); err == nil { + var cmd FFmpegCommand + if err := json.Unmarshal(cmdBytes, &cmd); err == nil { + return &cmd, nil + } + } + } + + return nil, fmt.Errorf("no ffmpeg command found in job config") +} + +// ToJSON converts FFmpegCommand to JSON string +func (cmd *FFmpegCommand) ToJSON() string { + data, err := json.MarshalIndent(cmd, "", " ") + if err != nil { + return "{}" + } + return string(data) +} + +// FromJSON creates FFmpegCommand from JSON string +func FFmpegCommandFromJSON(jsonStr string) (*FFmpegCommand, error) { + var cmd FFmpegCommand + err := json.Unmarshal([]byte(jsonStr), &cmd) + if err != nil { + return nil, fmt.Errorf("invalid JSON: %w", err) + } + return &cmd, nil +} + +// ToFullCommand converts FFmpegCommand to full command string +func (cmd *FFmpegCommand) ToFullCommand() string { + if cmd == nil { + return "" + } + + args := []string{cmd.Executable} + args = append(args, cmd.Args...) + + if cmd.InputFile != "" { + args = append(args, "-i", cmd.InputFile) + } + + if cmd.OutputFile != "" { + args = append(args, cmd.OutputFile) + } + + return strings.Join(args, " ") +} + +// ValidateCommandStructure performs deeper validation of command structure +func ValidateCommandStructure(cmd *FFmpegCommand) error { + if cmd == nil { + return fmt.Errorf("command cannot be nil") + } + + // Check for common FFmpeg patterns + hasInput := false + hasOutput := false + + for _, arg := range cmd.Args { + if arg == "-i" && cmd.InputFile != "" { + hasInput = true + } + } + + if cmd.InputFile != "" { + hasInput = true + } + + if cmd.OutputFile != "" { + hasOutput = true + } + + if !hasInput { + return fmt.Errorf("command must specify an input file") + } + + if !hasOutput { + return fmt.Errorf("command must specify an output file") + } + + // Check for conflicting options + if cmd.Options != nil { + if overwrite, exists := cmd.Options["overwrite"]; exists && overwrite == "false" { + if cmd.OutputFile != "" && !strings.Contains(cmd.OutputFile, "OUTPUT") { + // Real file path with overwrite disabled + return fmt.Errorf("cannot overwrite existing file with overwrite disabled") + } + } + } + + return nil +} diff --git a/internal/queue/execute_edit_job.go.wip b/internal/queue/execute_edit_job.go.wip new file mode 100644 index 0000000..9d60eff --- /dev/null +++ b/internal/queue/execute_edit_job.go.wip @@ -0,0 +1,113 @@ +package queue + +import ( + "bufio" + "context" + "encoding/json" + "fmt" + "os/exec" + + "git.leaktechnologies.dev/stu/VideoTools/internal/logging" + "git.leaktechnologies.dev/stu/VideoTools/internal/ui/utils" +) + +// ExecuteEditJob executes an editable job with dynamic FFmpeg command +func ExecuteEditJob(ctx context.Context, job *Job, progressCallback func(float64), ffmpegPath string) error { + logging.Debug(logging.CatSystem, "executing edit job %s: %s", job.ID, job.Title) + + // Get FFmpeg command from job config + if job.Config == nil { + return fmt.Errorf("edit job has no config") + } + + cmdInterface, exists := job.Config["ffmpeg_command"] + if !exists { + return fmt.Errorf("edit job has no ffmpeg_command in config") + } + + // Convert to FFmpegCommand + var cmd queue.FFmpegCommand + if cmdBytes, err := json.Marshal(cmdInterface); err == nil { + if err := json.Unmarshal(cmdBytes, &cmd); err != nil { + return fmt.Errorf("failed to parse FFmpeg command: %w", err) + } + } else { + return fmt.Errorf("failed to serialize FFmpeg command: %w", err) + } + + // Validate command + editManager := queue.NewEditJobManager(s.jobQueue) + if err := editManager.ValidateCommand(&cmd); err != nil { + return fmt.Errorf("invalid FFmpeg command: %w", err) + } + + // Build final command args + finalArgs := cmd.Args + if cmd.InputFile != "" { + finalArgs = append([]string{"-i", cmd.InputFile}, finalArgs...) + } + if cmd.OutputFile != "" { + finalArgs = append(finalArgs, cmd.OutputFile) + } + + // Execute FFmpeg command + ffmpegPath := utils.GetFFmpegPath() + fullCmd := append([]string{ffmpegPath}, finalArgs...) + + logging.Info(logging.CatFFMPEG, "Executing edit job: %v", fullCmd) + + // Create and execute command + execCmd := exec.CommandContext(ctx, fullCmd[0], fullCmd[1:]...) + + // Set up pipes for stdout/stderr + stdout, err := execCmd.StdoutPipe() + if err != nil { + return fmt.Errorf("failed to create stdout pipe: %w", err) + } + + stderr, err := execCmd.StderrPipe() + if err != nil { + return fmt.Errorf("failed to create stderr pipe: %w", err) + } + + // Start command + if err := execCmd.Start(); err != nil { + return fmt.Errorf("failed to start FFmpeg: %w", err) + } + + // Parse output for progress + progressParser := utils.NewFFmpegProgressParser() + + // Combine stdout and stderr for processing + go func() { + scanner := bufio.NewScanner(stdout) + for scanner.Scan() { + if progress := progressParser.ParseLine(scanner.Text()); progress >= 0 { + progressCallback(progress) + } + } + }() + + go func() { + scanner := bufio.NewScanner(stderr) + for scanner.Scan() { + if progress := progressParser.ParseLine(scanner.Text()); progress >= 0 { + progressCallback(progress) + } + // Log stderr for debugging + logging.Debug(logging.CatFFMPEG, "FFmpeg stderr: %s", scanner.Text()) + } + }() + + // Wait for command to complete + err = execCmd.Wait() + if err != nil { + return fmt.Errorf("FFmpeg execution failed: %w", err) + } + + // Mark job as completed + progressCallback(100.0) + logging.Info(logging.CatFFMPEG, "Edit job %s completed successfully", job.ID) + + return nil +} diff --git a/internal/ui/command_editor.go b/internal/ui/command_editor.go new file mode 100644 index 0000000..8a1c18f --- /dev/null +++ b/internal/ui/command_editor.go @@ -0,0 +1,352 @@ +package ui + +import ( + "fmt" + "strings" + "time" + + "fyne.io/fyne/v2" + "fyne.io/fyne/v2/container" + "fyne.io/fyne/v2/dialog" + "fyne.io/fyne/v2/layout" + "fyne.io/fyne/v2/theme" + "fyne.io/fyne/v2/widget" + "git.leaktechnologies.dev/stu/VideoTools/internal/queue" +) + +// CommandEditor provides UI for editing FFmpeg commands +type CommandEditor struct { + window fyne.Window + editManager queue.EditJobManager + jobID string + + // UI components + jsonEntry *widget.Entry + validateBtn *widget.Button + applyBtn *widget.Button + resetBtn *widget.Button + cancelBtn *widget.Button + statusLabel *widget.Label + historyList *widget.List + + // Data + editableJob *queue.EditableJob + editHistory []queue.EditHistoryEntry +} + +// CommandEditorConfig holds configuration for the command editor +type CommandEditorConfig struct { + Window fyne.Window + EditManager queue.EditJobManager + JobID string + Title string +} + +// NewCommandEditor creates a new command editor dialog +func NewCommandEditor(config CommandEditorConfig) *CommandEditor { + editor := &CommandEditor{ + window: config.Window, + editManager: config.EditManager, + jobID: config.JobID, + } + + // Load editable job + editableJob, err := editor.editManager.GetEditableJob(config.JobID) + if err != nil { + dialog.ShowError(fmt.Errorf("Failed to load job: %w", err), config.Window) + return nil + } + editor.editableJob = editableJob + + // Load edit history + history, err := editor.editManager.GetEditHistory(config.JobID) + if err == nil { + editor.editHistory = history + } + + editor.buildUI(config.Title) + return editor +} + +// buildUI creates the command editor interface +func (e *CommandEditor) buildUI(title string) { + // JSON editor with syntax highlighting + e.jsonEntry = widget.NewMultiLineEntry() + e.jsonEntry.SetPlaceHolder("FFmpeg command JSON will appear here...") + e.jsonEntry.TextStyle = fyne.TextStyle{Monospace: true} + + // Load current command + if e.editableJob.CurrentCommand != nil { + e.jsonEntry.SetText(e.editableJob.CurrentCommand.ToJSON()) + } + + // Command validation status + e.statusLabel = widget.NewLabel("Ready") + e.statusLabel.Importance = widget.MediumImportance + + // Action buttons + e.validateBtn = widget.NewButtonWithIcon("Validate", theme.ConfirmIcon(), e.validateCommand) + e.validateBtn.Importance = widget.MediumImportance + + e.applyBtn = widget.NewButtonWithIcon("Apply Changes", theme.ConfirmIcon(), e.applyChanges) + e.applyBtn.Importance = widget.HighImportance + e.applyBtn.Disable() + + e.resetBtn = widget.NewButtonWithIcon("Reset to Original", theme.ViewRefreshIcon(), e.resetToOriginal) + e.resetBtn.Importance = widget.MediumImportance + + e.cancelBtn = widget.NewButtonWithIcon("Cancel", theme.CancelIcon(), func() { + e.close() + }) + + // Edit history list + e.historyList = widget.NewList( + func() int { return len(e.editHistory) }, + func() fyne.CanvasObject { + return container.NewVBox( + widget.NewLabel("Timestamp"), + widget.NewLabel("Change Reason"), + widget.NewSeparator(), + ) + }, + func(id widget.ListItemID, obj fyne.CanvasObject) { + if id >= len(e.editHistory) { + return + } + + entry := e.editHistory[id] + vbox := obj.(*fyne.Container) + timestamp := vbox.Objects[0].(*widget.Label) + reason := vbox.Objects[1].(*widget.Label) + + timestamp.SetText(entry.Timestamp.Format(time.RFC822)) + reason.SetText(entry.ChangeReason) + + if entry.Applied { + timestamp.Importance = widget.SuccessImportance + } + }, + ) + + // Layout + content := container.NewHSplit( + container.NewVBox( + widget.NewCard("Command Editor", "", + container.NewVBox( + widget.NewLabel("Edit FFmpeg command in JSON format:"), + container.NewScroll(e.jsonEntry), + e.statusLabel, + container.NewHBox( + e.validateBtn, + e.applyBtn, + e.resetBtn, + layout.NewSpacer(), + e.cancelBtn, + ), + ), + ), + ), + container.NewVBox( + widget.NewCard("Edit History", "", e.historyList), + e.buildCommandPreview(), + ), + ) + content.Resize(fyne.NewSize(900, 600)) + + // Dialog + dlg := dialog.NewCustom(title, "", content, e.window) + dlg.Resize(fyne.NewSize(950, 650)) + dlg.Show() + + // Auto-validation on text change + e.jsonEntry.OnChanged = func(text string) { + e.applyBtn.Disable() + e.statusLabel.SetText("Unsaved changes") + e.statusLabel.Importance = widget.MediumImportance + } +} + +// validateCommand validates the current command +func (e *CommandEditor) validateCommand() { + jsonText := e.jsonEntry.Text + + cmd, err := queue.FFmpegCommandFromJSON(jsonText) + if err != nil { + e.statusLabel.SetText(fmt.Sprintf("Invalid JSON: %v", err)) + e.statusLabel.Importance = widget.DangerImportance + e.applyBtn.Disable() + return + } + + if err := e.editManager.ValidateCommand(cmd); err != nil { + e.statusLabel.SetText(fmt.Sprintf("Invalid command: %v", err)) + e.statusLabel.Importance = widget.DangerImportance + e.applyBtn.Disable() + return + } + + if err := queue.ValidateCommandStructure(cmd); err != nil { + e.statusLabel.SetText(fmt.Sprintf("Command structure error: %v", err)) + e.statusLabel.Importance = widget.DangerImportance + e.applyBtn.Disable() + return + } + + e.statusLabel.SetText("Valid command") + e.statusLabel.Importance = widget.SuccessImportance + e.applyBtn.Enable() +} + +// applyChanges applies the edited command +func (e *CommandEditor) applyChanges() { + jsonText := e.jsonEntry.Text + + cmd, err := queue.FFmpegCommandFromJSON(jsonText) + if err != nil { + dialog.ShowError(fmt.Errorf("Invalid JSON: %w", err), e.window) + return + } + + // Show reason dialog + reasonEntry := widget.NewEntry() + reasonEntry.SetPlaceHolder("Enter reason for change...") + + content := container.NewVBox( + widget.NewLabel("Please enter a reason for this change:"), + reasonEntry, + ) + buttons := container.NewHBox( + widget.NewButton("Cancel", func() {}), + widget.NewButton("Apply", func() { + reason := reasonEntry.Text + if reason == "" { + reason = "Manual edit via command editor" + } + + if err := e.editManager.UpdateJobCommand(e.jobID, cmd, reason); err != nil { + dialog.ShowError(fmt.Errorf("Failed to update job: %w", err), e.window) + return + } + + if err := e.editManager.ApplyEdit(e.jobID); err != nil { + dialog.ShowError(fmt.Errorf("Failed to apply edit: %w", err), e.window) + return + } + + dialog.ShowInformation("Success", "Command updated successfully", e.window) + e.refreshData() + e.close() + }), + ) + + reasonDlg := dialog.NewCustom("Apply Changes", "OK", content, e.window) + reasonDlg.SetOnClosed(func() { + // Handle button clicks manually + }) + + // Create a custom dialog layout + dialogContent := container.NewVBox(content, buttons) + customDlg := dialog.NewCustomWithoutButtons("Apply Changes", dialogContent, e.window) + customDlg.Show() + reasonDlg.Show() +} + +// resetToOriginal resets the command to original +func (e *CommandEditor) resetToOriginal() { + if e.editableJob.OriginalCommand == nil { + dialog.ShowInformation("Info", "No original command available", e.window) + return + } + + confirmDlg := dialog.NewConfirm("Reset Command", + "Are you sure you want to reset to the original command? This will discard all current changes.", + func(confirmed bool) { + if confirmed { + e.jsonEntry.SetText(e.editableJob.OriginalCommand.ToJSON()) + e.statusLabel.SetText("Reset to original") + e.statusLabel.Importance = widget.MediumImportance + e.applyBtn.Disable() + } + }, e.window) + confirmDlg.Show() +} + +// buildCommandPreview creates a preview of the command +func (e *CommandEditor) buildCommandPreview() fyne.CanvasObject { + previewLabel := widget.NewLabel("") + previewLabel.TextStyle = fyne.TextStyle{Monospace: true} + previewLabel.Wrapping = fyne.TextWrapBreak + + refreshPreview := func() { + jsonText := e.jsonEntry.Text + cmd, err := queue.FFmpegCommandFromJSON(jsonText) + if err != nil { + previewLabel.SetText("Invalid command") + return + } + previewLabel.SetText(cmd.ToFullCommand()) + } + + // Initial preview + refreshPreview() + + // Update preview on text change + e.jsonEntry.OnChanged = func(text string) { + refreshPreview() + e.applyBtn.Disable() + e.statusLabel.SetText("Unsaved changes") + e.statusLabel.Importance = widget.MediumImportance + } + + return widget.NewCard("Command Preview", "", + container.NewScroll(previewLabel)) +} + +// refreshData refreshes the editor data +func (e *CommandEditor) refreshData() { + // Reload editable job + editableJob, err := e.editManager.GetEditableJob(e.jobID) + if err == nil { + e.editableJob = editableJob + } + + // Reload history + history, err := e.editManager.GetEditHistory(e.jobID) + if err == nil { + e.editHistory = history + e.historyList.Refresh() + } +} + +// close closes the editor +func (e *CommandEditor) close() { + // Close dialog by finding parent dialog + // This is a workaround since Fyne doesn't expose direct dialog closing + for _, win := range fyne.CurrentApp().Driver().AllWindows() { + if win.Title() == "Command Editor" || strings.Contains(win.Title(), "Edit Job") { + win.Close() + break + } + } +} + +// ShowCommandEditorDialog shows a command editor for a specific job +func ShowCommandEditorDialog(window fyne.Window, editManager queue.EditJobManager, jobID, jobTitle string) { + config := CommandEditorConfig{ + Window: window, + EditManager: editManager, + JobID: jobID, + Title: fmt.Sprintf("Edit Job: %s", jobTitle), + } + + NewCommandEditor(config) +} + +// CreateCommandEditorButton creates a button that opens the command editor +func CreateCommandEditorButton(window fyne.Window, editManager queue.EditJobManager, jobID, jobTitle string) *widget.Button { + btn := widget.NewButtonWithIcon("Edit Command", theme.DocumentCreateIcon(), func() { + ShowCommandEditorDialog(window, editManager, jobID, jobTitle) + }) + btn.Importance = widget.MediumImportance + return btn +} diff --git a/main.go b/main.go index 5149975..eae3c04 100644 --- a/main.go +++ b/main.go @@ -72,7 +72,7 @@ var ( logsDirOnce sync.Once logsDirPath string feedbackBundler = utils.NewFeedbackBundler() - appVersion = "v0.1.0-dev21" + appVersion = "v0.1.0-dev22" hwAccelProbeOnce sync.Once hwAccelSupported atomic.Value // map[string]bool @@ -5724,7 +5724,6 @@ func (s *appState) executeUpscaleJob(ctx context.Context, job *queue.Job, progre return nil } - // buildFFmpegCommandFromJob builds an FFmpeg command string from a queue job with INPUT/OUTPUT placeholders func buildFFmpegCommandFromJob(job *queue.Job) string { if job == nil || job.Config == nil {