// Code generated by sqlc. DO NOT EDIT. // versions: // sqlc v1.31.1 // source: reacquisition.sql package dbq import ( "context" "github.com/jackc/pgx/v5/pgtype" ) const clearRecoveredReacquisitions = `-- name: ClearRecoveredReacquisitions :execrows DELETE FROM missing_reacquisitions r WHERE NOT EXISTS ( SELECT 1 FROM tracks WHERE tracks.album_id = r.album_id AND tracks.missing_since IS NOT NULL ) ` // Drops state for albums that no longer have any missing track — the files // came back, or the scanner adopted them at a new path (#2528). Deleting // rather than resetting counters means a future loss starts from a clean // budget, which is right: it is a new problem, not a continuation. func (q *Queries) ClearRecoveredReacquisitions(ctx context.Context) (int64, error) { result, err := q.db.Exec(ctx, clearRecoveredReacquisitions) if err != nil { return 0, err } return result.RowsAffected(), nil } const countAlbumsMissingWithoutMbid = `-- name: CountAlbumsMissingWithoutMbid :one SELECT COUNT(DISTINCT albums.id)::bigint FROM albums JOIN artists ON artists.id = albums.artist_id JOIN tracks ON tracks.album_id = albums.id WHERE tracks.missing_since IS NOT NULL AND (albums.mbid IS NULL OR artists.mbid IS NULL) ` // Albums with missing files that can never be auto-requested because nothing // identifies them to MusicBrainz. Surfaced on the admin card so the gap is // visible: silently doing nothing for these would read as the feature being // broken. func (q *Queries) CountAlbumsMissingWithoutMbid(ctx context.Context) (int64, error) { row := q.db.QueryRow(ctx, countAlbumsMissingWithoutMbid) var column_1 int64 err := row.Scan(&column_1) return column_1, err } const getReacquisitionForAlbums = `-- name: GetReacquisitionForAlbums :many SELECT album_id, attempts, last_attempt_at, last_request_id, gave_up_at, created_at, updated_at FROM missing_reacquisitions WHERE album_id = ANY($1::uuid[]) ` // State for the admin missing-files surface, so each directory group can say // whether a re-acquisition is in flight, waiting, or given up. func (q *Queries) GetReacquisitionForAlbums(ctx context.Context, albumIds []pgtype.UUID) ([]MissingReacquisition, error) { rows, err := q.db.Query(ctx, getReacquisitionForAlbums, albumIds) if err != nil { return nil, err } defer rows.Close() var items []MissingReacquisition for rows.Next() { var i MissingReacquisition if err := rows.Scan( &i.AlbumID, &i.Attempts, &i.LastAttemptAt, &i.LastRequestID, &i.GaveUpAt, &i.CreatedAt, &i.UpdatedAt, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const getReacquisitionSettings = `-- name: GetReacquisitionSettings :one SELECT id, enabled, grace_hours, backoff_base_hours, backoff_max_hours, max_attempts, max_per_pass, auto_approve FROM reacquisition_settings WHERE id = true ` // Auto re-acquisition of missing files (milestone #290). The unit is the // album: Lidarr acquires releases, and grouping collapses "40 missing files" // into "3 albums to ask for". func (q *Queries) GetReacquisitionSettings(ctx context.Context) (ReacquisitionSetting, error) { row := q.db.QueryRow(ctx, getReacquisitionSettings) var i ReacquisitionSetting err := row.Scan( &i.ID, &i.Enabled, &i.GraceHours, &i.BackoffBaseHours, &i.BackoffMaxHours, &i.MaxAttempts, &i.MaxPerPass, &i.AutoApprove, ) return i, err } const listAlbumsDueReacquisition = `-- name: ListAlbumsDueReacquisition :many SELECT albums.id AS album_id, albums.title AS album_title, albums.mbid AS album_mbid, artists.id AS artist_id, artists.name AS artist_name, artists.mbid AS artist_mbid, COUNT(tracks.id)::bigint AS missing_track_count, COALESCE(r.attempts, 0)::int AS attempts FROM albums JOIN artists ON artists.id = albums.artist_id JOIN tracks ON tracks.album_id = albums.id LEFT JOIN missing_reacquisitions r ON r.album_id = albums.id WHERE tracks.missing_since IS NOT NULL AND tracks.missing_since <= now() - make_interval(hours => $1::int) AND albums.mbid IS NOT NULL AND artists.mbid IS NOT NULL AND (r.gave_up_at IS NULL) AND ( r.last_attempt_at IS NULL OR r.last_attempt_at <= now() - make_interval(hours => LEAST( ($2::int * POWER(2, GREATEST(COALESCE(r.attempts, 0) - 1, 0)))::int, $3::int)) ) GROUP BY albums.id, albums.title, albums.mbid, artists.id, artists.name, artists.mbid, r.attempts, r.last_attempt_at ORDER BY r.last_attempt_at NULLS FIRST, albums.sort_title LIMIT $4 ` type ListAlbumsDueReacquisitionParams struct { GraceHours int32 BackoffBaseHours int32 BackoffMaxHours int32 PageLimit int32 } type ListAlbumsDueReacquisitionRow struct { AlbumID pgtype.UUID AlbumTitle string AlbumMbid *string ArtistID pgtype.UUID ArtistName string ArtistMbid *string MissingTrackCount int64 Attempts int32 } // The sweeper's selection. An album qualifies when: // // - it still has at least one track whose file has been missing longer than // the grace window. Measured on missing_since, which reconcile never // re-stamps (#2523), so it is a genuine "gone since" clock rather than // "when we last noticed"; // - MusicBrainz can name it. Both the album and its artist MBID are // required — Create rejects an album-kind request without them, and there // is nothing to ask Lidarr for anyway. Albums failing this are counted // separately (CountAlbumsMissingWithoutMbid) rather than vanishing; // - it has not spent its attempt budget (gave_up_at IS NULL); // - its backoff has elapsed: base * 2^(attempts-1) hours since the last // attempt, clamped to the configured maximum. First attempt (no row, or // last_attempt_at NULL) is always due. // // Oldest attempt first, never-attempted first, so a large loss drains in a // stable order across passes instead of re-picking the same head each time. func (q *Queries) ListAlbumsDueReacquisition(ctx context.Context, arg ListAlbumsDueReacquisitionParams) ([]ListAlbumsDueReacquisitionRow, error) { rows, err := q.db.Query(ctx, listAlbumsDueReacquisition, arg.GraceHours, arg.BackoffBaseHours, arg.BackoffMaxHours, arg.PageLimit, ) if err != nil { return nil, err } defer rows.Close() var items []ListAlbumsDueReacquisitionRow for rows.Next() { var i ListAlbumsDueReacquisitionRow if err := rows.Scan( &i.AlbumID, &i.AlbumTitle, &i.AlbumMbid, &i.ArtistID, &i.ArtistName, &i.ArtistMbid, &i.MissingTrackCount, &i.Attempts, ); err != nil { return nil, err } items = append(items, i) } if err := rows.Err(); err != nil { return nil, err } return items, nil } const markReacquisitionGaveUp = `-- name: MarkReacquisitionGaveUp :exec UPDATE missing_reacquisitions SET gave_up_at = now(), updated_at = now() WHERE album_id = $1 AND gave_up_at IS NULL ` // Stamped when the attempt budget is spent. Stored as a timestamp rather than // inferred from `attempts >= max_attempts` so the verdict survives an operator // later raising the maximum, and so the admin surface can say when. func (q *Queries) MarkReacquisitionGaveUp(ctx context.Context, albumID pgtype.UUID) error { _, err := q.db.Exec(ctx, markReacquisitionGaveUp, albumID) return err } const recordReacquisitionAttempt = `-- name: RecordReacquisitionAttempt :one INSERT INTO missing_reacquisitions (album_id, attempts, last_attempt_at, last_request_id) VALUES ($1, 1, now(), $2) ON CONFLICT (album_id) DO UPDATE SET attempts = missing_reacquisitions.attempts + 1, last_attempt_at = now(), last_request_id = COALESCE(EXCLUDED.last_request_id, missing_reacquisitions.last_request_id), updated_at = now() RETURNING album_id, attempts, last_attempt_at, last_request_id, gave_up_at, created_at, updated_at ` type RecordReacquisitionAttemptParams struct { AlbumID pgtype.UUID LastRequestID pgtype.UUID } // Bumps the attempt counter and stamps the clock the backoff measures from. // Upsert because the first attempt has no row yet. func (q *Queries) RecordReacquisitionAttempt(ctx context.Context, arg RecordReacquisitionAttemptParams) (MissingReacquisition, error) { row := q.db.QueryRow(ctx, recordReacquisitionAttempt, arg.AlbumID, arg.LastRequestID) var i MissingReacquisition err := row.Scan( &i.AlbumID, &i.Attempts, &i.LastAttemptAt, &i.LastRequestID, &i.GaveUpAt, &i.CreatedAt, &i.UpdatedAt, ) return i, err } const updateReacquisitionSettings = `-- name: UpdateReacquisitionSettings :one UPDATE reacquisition_settings SET enabled = $1, grace_hours = $2, backoff_base_hours = $3, backoff_max_hours = $4, max_attempts = $5, max_per_pass = $6, auto_approve = $7 WHERE id = true RETURNING id, enabled, grace_hours, backoff_base_hours, backoff_max_hours, max_attempts, max_per_pass, auto_approve ` type UpdateReacquisitionSettingsParams struct { Enabled bool GraceHours int32 BackoffBaseHours int32 BackoffMaxHours int32 MaxAttempts int32 MaxPerPass int32 AutoApprove bool } // Whole-row write from the admin card; the CHECKs in migration 0056 are the // validation, so a bad value fails loudly rather than being clamped silently. func (q *Queries) UpdateReacquisitionSettings(ctx context.Context, arg UpdateReacquisitionSettingsParams) (ReacquisitionSetting, error) { row := q.db.QueryRow(ctx, updateReacquisitionSettings, arg.Enabled, arg.GraceHours, arg.BackoffBaseHours, arg.BackoffMaxHours, arg.MaxAttempts, arg.MaxPerPass, arg.AutoApprove, ) var i ReacquisitionSetting err := row.Scan( &i.ID, &i.Enabled, &i.GraceHours, &i.BackoffBaseHours, &i.BackoffMaxHours, &i.MaxAttempts, &i.MaxPerPass, &i.AutoApprove, ) return i, err }