package uploads import ( "context" "encoding/json" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" ) // ChangeStatus deliberately does not expose pgx to use cases. func (st *Store) ChangeStatus(ctx context.Context, id uuid.UUID, from []Status, to Status) error { return st.Transition(ctx, id, from, to, nil) } // UpdateMetadata uses one guarded UPDATE; a concurrent decision is reported to the caller. func (st *Store) UpdateMetadata(ctx context.Context, id uuid.UUID, m Meta, edited bool, pl placement) error { raw, err := json.Marshal(m) if err != nil { return err } result, err := st.pool.Exec(ctx, `UPDATE upload_files SET metadata=$2, metadata_edited=$3, target_source_id=$4, target_rel_path=$5, status_reason=$6, matched_artist_id=$7, matched_album_id=$8, updated_at=now() WHERE id=$1 AND status IN ('pending_review','import_failed')`, id, raw, edited, pl.SourceID, pl.RelPath, nullable(pl.Reason), pl.ArtistID, pl.AlbumID) if err != nil { return fmt.Errorf("uploads: update: %w", err) } if result.RowsAffected() != 1 { return ErrWrongStatus } return nil } // Decide keeps quota release, status and reviewer metadata in the same transaction. func (st *Store) Decide(ctx context.Context, id, reviewer uuid.UUID, approve bool, reason string) error { from, to := []Status{StatusPendingReview, StatusImportFailed}, StatusRejected if approve { from, to, reason = []Status{StatusPendingReview}, StatusApproved, "" } return st.Transition(ctx, id, from, to, func(tx pgx.Tx) error { _, err := tx.Exec(ctx, `UPDATE upload_files SET reviewed_by=$2, reviewed_at=now(), status_reason=$3 WHERE id=$1`, id, reviewer, nullable(reason)) return err }) } func (st *Store) FailImport(ctx context.Context, id uuid.UUID, reason string) error { return st.Transition(ctx, id, []Status{StatusImporting}, StatusImportFailed, func(tx pgx.Tx) error { _, err := tx.Exec(ctx, `UPDATE upload_files SET status_reason=$2 WHERE id=$1`, id, reason) return err }) } func (st *Store) CompleteImport(ctx context.Context, id, sourceID uuid.UUID, rel string, mediaFileID uuid.UUID) error { return st.Transition(ctx, id, []Status{StatusImporting}, StatusImported, func(tx pgx.Tx) error { if _, err := tx.Exec(ctx, `UPDATE upload_files SET target_source_id=$2, target_rel_path=$3 WHERE id=$1`, id, sourceID, rel); err != nil { return err } return MarkImported(ctx, tx, id, mediaFileID) }) } func (st *Store) ResetImporting(ctx context.Context) error { _, err := st.pool.Exec(ctx, `UPDATE upload_files SET status='approved', updated_at=now() WHERE status='importing'`) return err } func (st *Store) SaveOutcome(ctx context.Context, f File, o outcome) error { return st.Transition(ctx, f.ID, []Status{StatusProcessing}, o.status, func(tx pgx.Tx) error { var ( tags, metaJSON []byte points []int32 ) if o.tags != nil { tags, _ = json.Marshal(o.tags) } if o.meta != nil { metaJSON, _ = json.Marshal(o.meta) } if o.print != nil { points = make([]int32, len(o.print.Points)) for i, p := range o.print.Points { points[i] = int32(p) } } p := o.probe _, err := tx.Exec(ctx, ` UPDATE upload_files SET sha256 = coalesce($2, sha256), format = NULLIF($3, ''), codec = NULLIF($4, ''), bitrate = NULLIF($5, 0), sample_rate = NULLIF($6, 0), bit_depth = NULLIF($7, 0), duration_ms = NULLIF($8, 0), tags = $9, metadata = $10, fingerprint = $11, duplicate_of_track_id = $12, duplicate_level = NULLIF($13, ''), possible_duplicate = $14, quality_upgrade = $20, target_source_id = $15, target_rel_path = $16, status_reason = NULLIF($17, ''), matched_artist_id = $18, matched_album_id = $19 WHERE id = $1`, f.ID, o.sha, p.Format, p.Codec, p.Bitrate, p.SampleRate, p.BitDepth, p.DurationMs, tags, metaJSON, points, o.dupTrack, o.dupLevel, o.possible, o.place.SourceID, o.place.RelPath, o.reason, o.place.ArtistID, o.place.AlbumID, o.quality) return err }) }