package engine import ( "context" "log" "sync" "time" "github.com/torrentclaw/torrentclaw-cli/internal/agent" ) // ActionFunc is called when the server signals an action on a task. type ActionFunc func(taskID string) // ProgressReporter aggregates progress from downloads and reports to the API. // It batches updates to avoid flooding the server. type ProgressReporter struct { agentClient *agent.Client interval time.Duration onCancel ActionFunc onPause ActionFunc onDeleteFiles ActionFunc onStreamRequested ActionFunc mu sync.Mutex latest map[string]*Task // taskID -> task with latest progress } // NewProgressReporter creates a reporter that flushes every interval. func NewProgressReporter(ac *agent.Client, interval time.Duration) *ProgressReporter { return &ProgressReporter{ agentClient: ac, interval: interval, latest: make(map[string]*Task), } } // SetCancelHandler sets the callback invoked when the server says a task is cancelled. func (r *ProgressReporter) SetCancelHandler(fn ActionFunc) { r.onCancel = fn } // SetPauseHandler sets the callback invoked when the server says a task is paused. func (r *ProgressReporter) SetPauseHandler(fn ActionFunc) { r.onPause = fn } // SetDeleteFilesHandler sets the callback for cancel+delete files. func (r *ProgressReporter) SetDeleteFilesHandler(fn ActionFunc) { r.onDeleteFiles = fn } // SetStreamRequestedHandler sets the callback for stream activation. func (r *ProgressReporter) SetStreamRequestedHandler(fn ActionFunc) { r.onStreamRequested = fn } // Track registers a task for progress tracking. func (r *ProgressReporter) Track(task *Task) { r.mu.Lock() defer r.mu.Unlock() r.latest[task.ID] = task } // Untrack removes a task from progress tracking. func (r *ProgressReporter) Untrack(taskID string) { r.mu.Lock() defer r.mu.Unlock() delete(r.latest, taskID) } // Run starts the periodic flush loop. Blocks until ctx is cancelled. func (r *ProgressReporter) Run(ctx context.Context) error { ticker := time.NewTicker(r.interval) defer ticker.Stop() for { select { case <-ctx.Done(): r.flush(context.Background()) return nil case <-ticker.C: r.flush(ctx) } } } func (r *ProgressReporter) flush(ctx context.Context) { r.mu.Lock() tasks := make([]*Task, 0, len(r.latest)) for _, t := range r.latest { tasks = append(tasks, t) } r.mu.Unlock() for _, task := range tasks { status := task.GetStatus() if status != StatusDownloading && status != StatusVerifying && status != StatusOrganizing && status != StatusSeeding && status != StatusCompleted && status != StatusFailed { continue } update := task.ToStatusUpdate() resp, err := r.agentClient.ReportStatus(ctx, update) if err != nil { log.Printf("[%s] progress report failed: %v", task.ID[:8], err) continue } // Handle server-side signals if resp.Cancelled { log.Printf("[%s] cancelled by user (via web)", task.ID[:8]) r.Untrack(task.ID) if resp.DeleteFiles && r.onDeleteFiles != nil { r.onDeleteFiles(task.ID) } else if r.onCancel != nil { r.onCancel(task.ID) } } else if resp.Paused { log.Printf("[%s] paused by user (via web)", task.ID[:8]) r.Untrack(task.ID) if r.onPause != nil { r.onPause(task.ID) } } if resp.StreamRequested && task.GetStreamURL() == "" { log.Printf("[%s] stream requested by user (via web)", task.ID[:8]) if r.onStreamRequested != nil { r.onStreamRequested(task.ID) } } } } // ReportFinal sends a final status update for a completed/failed task. func (r *ProgressReporter) ReportFinal(ctx context.Context, task *Task) { update := task.ToStatusUpdate() if _, err := r.agentClient.ReportStatus(ctx, update); err != nil { log.Printf("[%s] final report failed: %v", task.ID[:8], err) } r.Untrack(task.ID) }