Files
minstrel/internal/db/dbq/search.sql.go
T
bvandeusenandClaude Opus 5.5 8245f4406e
release / govulncheck (push) Successful in 17s
release / web (push) Failing after 17s
release / go (push) Successful in 1m11s
release / integration (push) Successful in 4m50s
release / android (push) Successful in 4m54s
release / Build signed APK (releases and dev) (push) Successful in 5m9s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Skipped
release / Verify release artifacts (tag releases only) (push) Skipped
feat: download lifecycle — stalled and failed downloads are given up and searched again; imported ones seed, then go (M502 #5523)
Ported from Fabled Librarian's System 104. A new lifecycle poller, every
ten minutes:
- a download the client fails is given up at once;
- one nobody shares, or still fetching its file list, past the stall
  setting (6 h) is asked for again three times, then given up;
- giving up removes the download with its files, holds the release back
  for a week (release_blocks.expires_at), ends the grab and searches the
  album again, with no limit per album;
- a running download the client stops listing for half an hour ends and is
  searched again, not held back; an empty list is not taken as gone;
- an imported download seeds to ratio 2.0 or 14 days, then is removed;
- a download the client would not remove is asked again every pass.

Every condition and act is one open row per (client, download, condition)
in download_interventions. Activity shows what is wrong with a download and
how often it was retried, why one ended, its seeding, and a banner for a
client that does not answer. A running download can be given up by hand
(set aside for good). The seed ratio, seed days and stall hours are on the
search settings card. The download client lookup moves to
acquire.DownloadClientFor, shared by the importer and the lifecycle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-10 15:27:22 -04:00

650 lines
17 KiB
Go

// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: search.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const blockRelease = `-- name: BlockRelease :exec
INSERT INTO release_blocks (info_hash, name, reason, detail, want_id, blocked_by, expires_at)
VALUES ($1, $2, $3, $4,
$5, $6, $7)
ON CONFLICT (info_hash) DO UPDATE
SET reason = EXCLUDED.reason, detail = EXCLUDED.detail, blocked_at = now(),
expires_at = EXCLUDED.expires_at, blocked_by = EXCLUDED.blocked_by
WHERE release_blocks.expires_at IS NOT NULL OR EXCLUDED.expires_at IS NULL
`
type BlockReleaseParams struct {
InfoHash string
Name string
Reason string
Detail string
WantID pgtype.UUID
BlockedBy pgtype.UUID
ExpiresAt pgtype.Timestamptz
}
// A release set aside. expires_at NULL is for good. A block for good is never
// shortened by a timed one: only a timed block, or another for good, is
// replaced.
func (q *Queries) BlockRelease(ctx context.Context, arg BlockReleaseParams) error {
_, err := q.db.Exec(ctx, blockRelease,
arg.InfoHash,
arg.Name,
arg.Reason,
arg.Detail,
arg.WantID,
arg.BlockedBy,
arg.ExpiresAt,
)
return err
}
const deleteWantCandidates = `-- name: DeleteWantCandidates :exec
DELETE FROM want_candidates WHERE want_id = $1
`
func (q *Queries) DeleteWantCandidates(ctx context.Context, wantID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteWantCandidates, wantID)
return err
}
const editionTrackSpan = `-- name: EditionTrackSpan :one
SELECT COALESCE(MIN(track_count) FILTER (WHERE status = 'Official'), MIN(track_count), 0)::int AS min_tracks,
COALESCE(MAX(track_count), 0)::int AS max_tracks
FROM mb_releases
WHERE release_group_mbid = $1 AND track_count > 0
`
type EditionTrackSpanRow struct {
MinTracks int32
MaxTracks int32
}
// The fewest tracks an official edition of the group has (any edition when
// none is official), and the most any has. Zero when none is catalogued.
func (q *Queries) EditionTrackSpan(ctx context.Context, releaseGroupMbid string) (EditionTrackSpanRow, error) {
row := q.db.QueryRow(ctx, editionTrackSpan, releaseGroupMbid)
var i EditionTrackSpanRow
err := row.Scan(&i.MinTracks, &i.MaxTracks)
return i, err
}
const endGrab = `-- name: EndGrab :exec
UPDATE grabs
SET ended_at = now(), end_reason = $1, end_detail = $2
WHERE id = $3 AND ended_at IS NULL
`
type EndGrabParams struct {
EndReason string
EndDetail string
ID pgtype.UUID
}
func (q *Queries) EndGrab(ctx context.Context, arg EndGrabParams) error {
_, err := q.db.Exec(ctx, endGrab, arg.EndReason, arg.EndDetail, arg.ID)
return err
}
const getAcquisitionSettings = `-- name: GetAcquisitionSettings :one
SELECT id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours FROM acquisition_settings WHERE id = true
`
// Searching for wants and grabbing releases (M502 #5521).
func (q *Queries) GetAcquisitionSettings(ctx context.Context) (AcquisitionSetting, error) {
row := q.db.QueryRow(ctx, getAcquisitionSettings)
var i AcquisitionSetting
err := row.Scan(
&i.ID,
&i.AutoGrab,
&i.SearchIntervalHours,
&i.AllowedQualities,
&i.LastPassAt,
&i.LastPass,
&i.UpdatedAt,
&i.ImportRoot,
&i.SeedRatio,
&i.SeedDays,
&i.StallHours,
)
return i, err
}
const getLiveGrab = `-- name: GetLiveGrab :one
SELECT id, want_id, info_hash, name, key, indexer, source, client, save_path, quality, origin, forced, grabbed_by, grabbed_at, ended_at, end_reason, end_detail, progress, import_state, import_reason, import_detail, import_forced, import_attempts, import_updated_at, imported_at, imported_files, destination, seeding, ratio, seed_released_at FROM grabs WHERE want_id = $1 AND ended_at IS NULL
`
func (q *Queries) GetLiveGrab(ctx context.Context, wantID pgtype.UUID) (Grab, error) {
row := q.db.QueryRow(ctx, getLiveGrab, wantID)
var i Grab
err := row.Scan(
&i.ID,
&i.WantID,
&i.InfoHash,
&i.Name,
&i.Key,
&i.Indexer,
&i.Source,
&i.Client,
&i.SavePath,
&i.Quality,
&i.Origin,
&i.Forced,
&i.GrabbedBy,
&i.GrabbedAt,
&i.EndedAt,
&i.EndReason,
&i.EndDetail,
&i.Progress,
&i.ImportState,
&i.ImportReason,
&i.ImportDetail,
&i.ImportForced,
&i.ImportAttempts,
&i.ImportUpdatedAt,
&i.ImportedAt,
&i.ImportedFiles,
&i.Destination,
&i.Seeding,
&i.Ratio,
&i.SeedReleasedAt,
)
return i, err
}
const getWantCandidate = `-- name: GetWantCandidate :one
SELECT want_id, key, name, info_hash, indexer, source, size_bytes, seeders, files_count, format, quality, year, verdict, reason, reasons, folded_from, rank, searched_at FROM want_candidates WHERE want_id = $1 AND key = $2
`
type GetWantCandidateParams struct {
WantID pgtype.UUID
Key string
}
func (q *Queries) GetWantCandidate(ctx context.Context, arg GetWantCandidateParams) (WantCandidate, error) {
row := q.db.QueryRow(ctx, getWantCandidate, arg.WantID, arg.Key)
var i WantCandidate
err := row.Scan(
&i.WantID,
&i.Key,
&i.Name,
&i.InfoHash,
&i.Indexer,
&i.Source,
&i.SizeBytes,
&i.Seeders,
&i.FilesCount,
&i.Format,
&i.Quality,
&i.Year,
&i.Verdict,
&i.Reason,
&i.Reasons,
&i.FoldedFrom,
&i.Rank,
&i.SearchedAt,
)
return i, err
}
const getWantForSearch = `-- name: GetWantForSearch :one
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason, w.fulfilled_at,
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
FROM wants w
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
WHERE w.id = $1
`
type GetWantForSearchRow struct {
ID pgtype.UUID
ReleaseGroupMbid string
ArtistMbid string
ArtistName string
Title string
Reason string
FulfilledAt pgtype.Timestamptz
PrimaryType string
SecondaryTypes []string
FirstReleaseDate string
}
func (q *Queries) GetWantForSearch(ctx context.Context, id pgtype.UUID) (GetWantForSearchRow, error) {
row := q.db.QueryRow(ctx, getWantForSearch, id)
var i GetWantForSearchRow
err := row.Scan(
&i.ID,
&i.ReleaseGroupMbid,
&i.ArtistMbid,
&i.ArtistName,
&i.Title,
&i.Reason,
&i.FulfilledAt,
&i.PrimaryType,
&i.SecondaryTypes,
&i.FirstReleaseDate,
)
return i, err
}
const insertGrab = `-- name: InsertGrab :one
INSERT INTO grabs (want_id, info_hash, name, key, indexer, source, client, save_path, quality,
origin, forced, grabbed_by)
VALUES ($1, $2, $3, $4, $5,
$6, $7, $8, $9,
$10, $11, $12)
RETURNING id, want_id, info_hash, name, key, indexer, source, client, save_path, quality, origin, forced, grabbed_by, grabbed_at, ended_at, end_reason, end_detail, progress, import_state, import_reason, import_detail, import_forced, import_attempts, import_updated_at, imported_at, imported_files, destination, seeding, ratio, seed_released_at
`
type InsertGrabParams struct {
WantID pgtype.UUID
InfoHash string
Name string
Key string
Indexer string
Source string
Client string
SavePath string
Quality string
Origin string
Forced bool
GrabbedBy pgtype.UUID
}
func (q *Queries) InsertGrab(ctx context.Context, arg InsertGrabParams) (Grab, error) {
row := q.db.QueryRow(ctx, insertGrab,
arg.WantID,
arg.InfoHash,
arg.Name,
arg.Key,
arg.Indexer,
arg.Source,
arg.Client,
arg.SavePath,
arg.Quality,
arg.Origin,
arg.Forced,
arg.GrabbedBy,
)
var i Grab
err := row.Scan(
&i.ID,
&i.WantID,
&i.InfoHash,
&i.Name,
&i.Key,
&i.Indexer,
&i.Source,
&i.Client,
&i.SavePath,
&i.Quality,
&i.Origin,
&i.Forced,
&i.GrabbedBy,
&i.GrabbedAt,
&i.EndedAt,
&i.EndReason,
&i.EndDetail,
&i.Progress,
&i.ImportState,
&i.ImportReason,
&i.ImportDetail,
&i.ImportForced,
&i.ImportAttempts,
&i.ImportUpdatedAt,
&i.ImportedAt,
&i.ImportedFiles,
&i.Destination,
&i.Seeding,
&i.Ratio,
&i.SeedReleasedAt,
)
return i, err
}
const insertWantCandidate = `-- name: InsertWantCandidate :exec
INSERT INTO want_candidates (want_id, key, name, info_hash, indexer, source, size_bytes, seeders,
files_count, format, quality, year, verdict, reason, reasons, folded_from, rank)
VALUES ($1, $2, $3, $4, $5,
$6, $7, $8, $9, $10,
$11, $12, $13, $14, $15,
$16, $17)
ON CONFLICT (want_id, key) DO NOTHING
`
type InsertWantCandidateParams struct {
WantID pgtype.UUID
Key string
Name string
InfoHash string
Indexer string
Source string
SizeBytes int64
Seeders int32
FilesCount int32
Format string
Quality string
Year int32
Verdict string
Reason string
Reasons []byte
FoldedFrom []string
Rank int32
}
func (q *Queries) InsertWantCandidate(ctx context.Context, arg InsertWantCandidateParams) error {
_, err := q.db.Exec(ctx, insertWantCandidate,
arg.WantID,
arg.Key,
arg.Name,
arg.InfoHash,
arg.Indexer,
arg.Source,
arg.SizeBytes,
arg.Seeders,
arg.FilesCount,
arg.Format,
arg.Quality,
arg.Year,
arg.Verdict,
arg.Reason,
arg.Reasons,
arg.FoldedFrom,
arg.Rank,
)
return err
}
const listReleaseBlocks = `-- name: ListReleaseBlocks :many
SELECT info_hash, name, reason, detail, want_id, blocked_by, blocked_at, expires_at FROM release_blocks
WHERE expires_at IS NULL OR expires_at > now()
ORDER BY blocked_at DESC
`
// Every release set aside and still held back: a stalled one lifts by itself
// at expires_at (#5523).
func (q *Queries) ListReleaseBlocks(ctx context.Context) ([]ReleaseBlock, error) {
rows, err := q.db.Query(ctx, listReleaseBlocks)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ReleaseBlock
for rows.Next() {
var i ReleaseBlock
if err := rows.Scan(
&i.InfoHash,
&i.Name,
&i.Reason,
&i.Detail,
&i.WantID,
&i.BlockedBy,
&i.BlockedAt,
&i.ExpiresAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWantCandidates = `-- name: ListWantCandidates :many
SELECT want_id, key, name, info_hash, indexer, source, size_bytes, seeders, files_count, format, quality, year, verdict, reason, reasons, folded_from, rank, searched_at FROM want_candidates WHERE want_id = $1 ORDER BY rank, key
`
func (q *Queries) ListWantCandidates(ctx context.Context, wantID pgtype.UUID) ([]WantCandidate, error) {
rows, err := q.db.Query(ctx, listWantCandidates, wantID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []WantCandidate
for rows.Next() {
var i WantCandidate
if err := rows.Scan(
&i.WantID,
&i.Key,
&i.Name,
&i.InfoHash,
&i.Indexer,
&i.Source,
&i.SizeBytes,
&i.Seeders,
&i.FilesCount,
&i.Format,
&i.Quality,
&i.Year,
&i.Verdict,
&i.Reason,
&i.Reasons,
&i.FoldedFrom,
&i.Rank,
&i.SearchedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listWantsDueForSearch = `-- name: ListWantsDueForSearch :many
SELECT w.id, w.release_group_mbid, w.artist_name, w.title, w.reason,
COALESCE(NULLIF(w.primary_type, ''), g.primary_type, '')::text AS primary_type,
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date
FROM wants w
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
WHERE w.fulfilled_at IS NULL
AND NOT EXISTS (SELECT 1 FROM grabs gr WHERE gr.want_id = w.id AND gr.ended_at IS NULL)
AND (w.searched_at IS NULL OR w.searched_at < now() - make_interval(hours => $1::int))
ORDER BY w.searched_at IS NOT NULL,
CASE w.reason WHEN 'request' THEN 0 WHEN 'missing' THEN 1 ELSE 2 END,
w.searched_at NULLS FIRST, w.wanted_at, w.id
LIMIT $2
`
type ListWantsDueForSearchParams struct {
Hours int32
MaxRows int32
}
type ListWantsDueForSearchRow struct {
ID pgtype.UUID
ReleaseGroupMbid string
ArtistName string
Title string
Reason string
PrimaryType string
SecondaryTypes []string
FirstReleaseDate string
}
// Open wants with no download running that were never searched or not since
// @hours ago: never-searched first, then requests, then missing files, then
// followed artists' releases.
func (q *Queries) ListWantsDueForSearch(ctx context.Context, arg ListWantsDueForSearchParams) ([]ListWantsDueForSearchRow, error) {
rows, err := q.db.Query(ctx, listWantsDueForSearch, arg.Hours, arg.MaxRows)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListWantsDueForSearchRow
for rows.Next() {
var i ListWantsDueForSearchRow
if err := rows.Scan(
&i.ID,
&i.ReleaseGroupMbid,
&i.ArtistName,
&i.Title,
&i.Reason,
&i.PrimaryType,
&i.SecondaryTypes,
&i.FirstReleaseDate,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const markCandidatesBlocked = `-- name: MarkCandidatesBlocked :exec
UPDATE want_candidates
SET verdict = 'blocked', reason = $1,
reasons = jsonb_build_array(jsonb_build_object('kind', 'hard', 'code', 'blocked', 'text', $1::text))
WHERE info_hash = $2
`
type MarkCandidatesBlockedParams struct {
Reason string
InfoHash string
}
func (q *Queries) MarkCandidatesBlocked(ctx context.Context, arg MarkCandidatesBlockedParams) error {
_, err := q.db.Exec(ctx, markCandidatesBlocked, arg.Reason, arg.InfoHash)
return err
}
const markCandidatesUnblocked = `-- name: MarkCandidatesUnblocked :exec
UPDATE want_candidates
SET verdict = 'review', reason = $1,
reasons = jsonb_build_array(jsonb_build_object('kind', 'soft', 'code', 'unblocked', 'text', $1::text))
WHERE info_hash = $2 AND verdict = 'blocked'
`
type MarkCandidatesUnblockedParams struct {
Reason string
InfoHash string
}
func (q *Queries) MarkCandidatesUnblocked(ctx context.Context, arg MarkCandidatesUnblockedParams) error {
_, err := q.db.Exec(ctx, markCandidatesUnblocked, arg.Reason, arg.InfoHash)
return err
}
const recordSearchPass = `-- name: RecordSearchPass :exec
UPDATE acquisition_settings SET last_pass_at = now(), last_pass = $1 WHERE id = true
`
func (q *Queries) RecordSearchPass(ctx context.Context, report []byte) error {
_, err := q.db.Exec(ctx, recordSearchPass, report)
return err
}
const recordWantSearch = `-- name: RecordWantSearch :exec
UPDATE wants
SET searched_at = now(),
search_outcome = $1,
search_considered = $2,
best_release = $3,
best_verdict = $4,
search_error = $5,
updated_at = now()
WHERE id = $6
`
type RecordWantSearchParams struct {
Outcome string
Considered int32
BestRelease string
BestVerdict string
SearchError string
ID pgtype.UUID
}
func (q *Queries) RecordWantSearch(ctx context.Context, arg RecordWantSearchParams) error {
_, err := q.db.Exec(ctx, recordWantSearch,
arg.Outcome,
arg.Considered,
arg.BestRelease,
arg.BestVerdict,
arg.SearchError,
arg.ID,
)
return err
}
const unblockRelease = `-- name: UnblockRelease :execrows
DELETE FROM release_blocks WHERE info_hash = $1
`
func (q *Queries) UnblockRelease(ctx context.Context, infoHash string) (int64, error) {
result, err := q.db.Exec(ctx, unblockRelease, infoHash)
if err != nil {
return 0, err
}
return result.RowsAffected(), nil
}
const updateAcquisitionSettings = `-- name: UpdateAcquisitionSettings :one
UPDATE acquisition_settings
SET auto_grab = $1,
search_interval_hours = $2,
allowed_qualities = $3,
import_root = $4,
seed_ratio = $5,
seed_days = $6,
stall_hours = $7,
updated_at = now()
WHERE id = true
RETURNING id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at, import_root, seed_ratio, seed_days, stall_hours
`
type UpdateAcquisitionSettingsParams struct {
AutoGrab bool
SearchIntervalHours int32
AllowedQualities []string
ImportRoot string
SeedRatio float32
SeedDays int32
StallHours int32
}
func (q *Queries) UpdateAcquisitionSettings(ctx context.Context, arg UpdateAcquisitionSettingsParams) (AcquisitionSetting, error) {
row := q.db.QueryRow(ctx, updateAcquisitionSettings,
arg.AutoGrab,
arg.SearchIntervalHours,
arg.AllowedQualities,
arg.ImportRoot,
arg.SeedRatio,
arg.SeedDays,
arg.StallHours,
)
var i AcquisitionSetting
err := row.Scan(
&i.ID,
&i.AutoGrab,
&i.SearchIntervalHours,
&i.AllowedQualities,
&i.LastPassAt,
&i.LastPass,
&i.UpdatedAt,
&i.ImportRoot,
&i.SeedRatio,
&i.SeedDays,
&i.StallHours,
)
return i, err
}