package store import ( "context" "errors" "fmt" "github.com/jackc/pgx/v5" ) // SetAutoSummarize sets the user's auto/manual summarization mode. TRUE = // automatic (every new video is summarized by `tapir run`); FALSE = manual (the // user queues videos individually). Per-user, not global (ADR-012). Scoped via // withUser, so RLS confines the UPDATE to the calling user's own row. func (s *Store) SetAutoSummarize(ctx context.Context, userID string, enabled bool) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { // Ensure the row exists (FK/identity target) before the UPDATE — mirrors // the Deliver/UpsertVideo paths, so toggling mode works even before the // first summary lands. if _, err := tx.Exec(ctx, `INSERT INTO users (id) VALUES ($1) ON CONFLICT (id) DO NOTHING`, userID); err != nil { return fmt.Errorf("store: upsert user: %w", err) } if _, err := tx.Exec(ctx, `UPDATE users SET auto_summarize = $1 WHERE id = $2`, enabled, userID); err != nil { return fmt.Errorf("store: set auto summarize: %w", err) } return nil }) } // GetAutoSummarize reports the user's summarization mode (TRUE = automatic). An // absent user row reads as FALSE (manual), the safe default. Scoped via withUser. func (s *Store) GetAutoSummarize(ctx context.Context, userID string) (bool, error) { var enabled bool if err := s.withUser(ctx, userID, func(tx pgx.Tx) error { err := tx.QueryRow(ctx, `SELECT auto_summarize FROM users WHERE id = $1`, userID).Scan(&enabled) if errors.Is(err, pgx.ErrNoRows) { enabled = false return nil } return err }); err != nil { return false, fmt.Errorf("store: get auto summarize: %w", err) } return enabled, nil } // RequestSummarize queues a single video for manual summarization by setting its // summarize_requested flag. The next `tapir run` picks it up and clears the flag. // Returns ErrNotFound when the video does not exist or is not owned by the user // (RLS hides another user's row, so the UPDATE matches zero rows). Scoped via // withUser. func (s *Store) RequestSummarize(ctx context.Context, userID, videoID string) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { ct, err := tx.Exec(ctx, `UPDATE videos SET summarize_requested = TRUE WHERE id = $1`, videoID) if err != nil { return fmt.Errorf("store: request summarize: %w", err) } if ct.RowsAffected() == 0 { return ErrNotFound } return nil }) } // RequestedVideoIDs returns the set of the user's video ids currently flagged for // manual summarization. The run loop loads it once per pass (mirroring // SeenVideoIDs) to decide which discovered videos to process in manual mode. // Scoped by user_id. func (s *Store) RequestedVideoIDs(ctx context.Context, userID string) (map[string]bool, error) { requested := make(map[string]bool) if err := s.withUser(ctx, userID, func(tx pgx.Tx) error { rows, err := tx.Query(ctx, `SELECT id FROM videos WHERE user_id = $1 AND summarize_requested = TRUE`, userID) if err != nil { return fmt.Errorf("store: requested video ids: %w", err) } defer rows.Close() for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return fmt.Errorf("store: scan requested id: %w", err) } requested[id] = true } if err := rows.Err(); err != nil { return fmt.Errorf("store: iterate requested ids: %w", err) } return nil }); err != nil { return nil, err } return requested, nil } // ClearSummarizeRequested resets a video's manual queue flag, called by the run // loop after a queued video is successfully summarized so it is not re-processed // and the list view drops the "Queued" chip. Scoped via withUser. func (s *Store) ClearSummarizeRequested(ctx context.Context, userID, videoID string) error { return s.withUser(ctx, userID, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `UPDATE videos SET summarize_requested = FALSE WHERE id = $1`, videoID); err != nil { return fmt.Errorf("store: clear summarize requested: %w", err) } return nil }) }