package repository import ( "database/sql" "vidarchive/internal/models" ) type DownloadRepository struct { db *sql.DB } func NewDownloadRepository(db *sql.DB) *DownloadRepository { return &DownloadRepository{db: db} } func (r *DownloadRepository) Create(d *models.Download) error { result, err := r.db.Exec( `INSERT INTO downloads (url, status, preset_id, format_override, custom_flags, output_dir, subscription_id) VALUES (?, ?, ?, ?, ?, ?, ?)`, d.URL, d.Status, d.PresetID, d.FormatOverride, d.CustomFlags, d.OutputDir, d.SubscriptionID, ) if err != nil { return err } d.ID, _ = result.LastInsertId() return nil } func (r *DownloadRepository) GetByID(id int64) (*models.Download, error) { row := r.db.QueryRow( `SELECT id, url, status, logs, error_message, preset_id, format_override, custom_flags, output_dir, subscription_id, started_at, completed_at, created_at FROM downloads WHERE id = ?`, id, ) return scanDownload(row) } func (r *DownloadRepository) GetAll(status, sortBy string) ([]*models.Download, error) { query := `SELECT id, url, status, logs, error_message, preset_id, format_override, custom_flags, output_dir, subscription_id, started_at, completed_at, created_at FROM downloads WHERE 1=1` var args []interface{} if status != "" && status != "all" { query += ` AND status = ?` args = append(args, status) } switch sortBy { case "date": query += ` ORDER BY created_at DESC` case "status": query += ` ORDER BY status, created_at DESC` default: query += ` ORDER BY created_at DESC` } rows, err := r.db.Query(query, args...) if err != nil { return nil, err } defer rows.Close() var downloads []*models.Download for rows.Next() { d, err := scanDownload(rows) if err != nil { return nil, err } downloads = append(downloads, d) } return downloads, rows.Err() } func (r *DownloadRepository) GetQueued(limit int) ([]*models.Download, error) { rows, err := r.db.Query( `SELECT id, url, status, logs, error_message, preset_id, format_override, custom_flags, output_dir, subscription_id, started_at, completed_at, created_at FROM downloads WHERE status = 'queued' ORDER BY created_at ASC LIMIT ?`, limit, ) if err != nil { return nil, err } defer rows.Close() var downloads []*models.Download for rows.Next() { d, err := scanDownload(rows) if err != nil { return nil, err } downloads = append(downloads, d) } return downloads, rows.Err() } func (r *DownloadRepository) UpdateStatus(id int64, status string) error { _, err := r.db.Exec(`UPDATE downloads SET status = ? WHERE id = ?`, status, id) return err } func (r *DownloadRepository) AppendLogs(id int64, logs string) error { _, err := r.db.Exec( `UPDATE downloads SET logs = COALESCE(logs, '') || ? WHERE id = ?`, logs, id, ) return err } // MarkStarted atomically transitions a download from 'queued' to 'downloading'. // It reports whether this call actually claimed it: false means another worker // already started it, so the caller must not process it again. func (r *DownloadRepository) MarkStarted(id int64) (bool, error) { res, err := r.db.Exec( `UPDATE downloads SET status = 'downloading', started_at = CURRENT_TIMESTAMP WHERE id = ? AND status = 'queued'`, id, ) if err != nil { return false, err } n, err := res.RowsAffected() if err != nil { return false, err } return n > 0, nil } func (r *DownloadRepository) MarkCompleted(id int64, status string) error { _, err := r.db.Exec( `UPDATE downloads SET status = ?, completed_at = CURRENT_TIMESTAMP WHERE id = ?`, status, id, ) return err } func (r *DownloadRepository) MarkError(id int64, errMsg string) error { _, err := r.db.Exec( `UPDATE downloads SET status = 'error', error_message = ? WHERE id = ?`, errMsg, id, ) return err } // HasActiveForSubscription reports whether the given subscription already has a // download that is queued or in progress, so the scheduler can avoid stacking a // second run on top of one that hasn't finished. func (r *DownloadRepository) HasActiveForSubscription(subID int64) (bool, error) { var n int err := r.db.QueryRow( `SELECT COUNT(*) FROM downloads WHERE subscription_id = ? AND status IN ('queued', 'downloading')`, subID, ).Scan(&n) if err != nil { return false, err } return n > 0, nil } func (r *DownloadRepository) Delete(id int64) error { _, err := r.db.Exec(`DELETE FROM downloads WHERE id = ?`, id) return err } func (r *DownloadRepository) DeleteAll() error { _, err := r.db.Exec(`DELETE FROM downloads`) return err } func (r *DownloadRepository) UpdateStatusWhere(oldStatus, newStatus string) error { _, err := r.db.Exec( `UPDATE downloads SET status = ? WHERE status = ?`, newStatus, oldStatus, ) return err } func scanDownload(row interface{ Scan(...interface{}) error }) (*models.Download, error) { var d models.Download err := row.Scan( &d.ID, &d.URL, &d.Status, &d.Logs, &d.ErrorMessage, &d.PresetID, &d.FormatOverride, &d.CustomFlags, &d.OutputDir, &d.SubscriptionID, &d.StartedAt, &d.CompletedAt, &d.CreatedAt, ) if err != nil { return nil, err } return &d, nil }