VideoTools/internal/queue/edit.go
Stu Leak 2332f2e9ca fix: update main menu version display to dev22
- Update appVersion constant from dev21 to dev22
- Ensures main menu footer and About dialog show correct version
- Completes dev22 release preparation

All build fixes applied and version correctly displayed.
2026-01-03 13:58:22 -05:00

363 lines
9.6 KiB
Go

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
}