- 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.
363 lines
9.6 KiB
Go
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
|
|
}
|