feat: search and grab — music release names, a quality ladder, a paced search schedule and one live grab per want (M502 #5521)
release / govulncheck (push) Successful in 21s
release / go (push) Failing after 33s
release / web (push) Successful in 56s
release / integration (push) Failing after 4m48s
release / android (push) Successful in 5m6s
release / Build signed APK (releases and dev) (push) Successful in 5m16s
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
release / govulncheck (push) Successful in 21s
release / go (push) Failing after 33s
release / web (push) Successful in 56s
release / integration (push) Failing after 4m48s
release / android (push) Successful in 5m6s
release / Build signed APK (releases and dev) (push) Successful in 5m16s
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
- internal/release: reads a release name (format, bitrate, bit depth and sample rate, source, year, video, discography) and judges it against the album wanted: the artist and title must be in it; video rips, discographies, other versions (drumless, instrumental, karaoke, remix, live unless the album is live…) and singles or EPs for an album are refused. Quality tiers on Lidarr's ladder; a .torrent's file list is read for format, video files and a track count short of the album. - internal/acquire: fan-out across every search source with a narrowed search kept, a process-wide pacer per source, every candidate judged and kept with its reasons, and the grab: one live grab per want, tie-broken (a person's grab beats the schedule's; a better release replaces a worse one unless it is nearly done). Each download lands in its own folder under the client's downloads folder. - internal/wanted: the search schedule, every ten minutes, never-searched first, then requests, missing files and followed artists, taking the best release when auto-grab is on (the default). - Migration 0082: acquisition_settings (auto-grab, interval, allowed qualities seeded from Lidarr's "Any"), want search columns, want_candidates, grabs, release_blocks. - Web: the Wanted page shows each want's last search, a Search button and every release found with its verdict (Grab, Grab anyway, Never this one, Allow again). Integrations gains "Searching and grabbing". Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -217,6 +217,16 @@ type AcquisitionService struct {
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type AcquisitionSetting struct {
|
||||
ID bool
|
||||
AutoGrab bool
|
||||
SearchIntervalHours int32
|
||||
AllowedQualities []string
|
||||
LastPassAt pgtype.Timestamptz
|
||||
LastPass []byte
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Album struct {
|
||||
ID pgtype.UUID
|
||||
Title string
|
||||
@@ -420,6 +430,26 @@ type GeneralLikesArtist struct {
|
||||
LikedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type Grab struct {
|
||||
ID pgtype.UUID
|
||||
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
|
||||
GrabbedAt pgtype.Timestamptz
|
||||
EndedAt pgtype.Timestamptz
|
||||
EndReason string
|
||||
EndDetail string
|
||||
}
|
||||
|
||||
type LibraryChange struct {
|
||||
ID int64
|
||||
EntityType string
|
||||
@@ -652,6 +682,16 @@ type RegistrationSetting struct {
|
||||
Mode string
|
||||
}
|
||||
|
||||
type ReleaseBlock struct {
|
||||
InfoHash string
|
||||
Name string
|
||||
Reason string
|
||||
Detail string
|
||||
WantID pgtype.UUID
|
||||
BlockedBy pgtype.UUID
|
||||
BlockedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type ReleaseTypeProfile struct {
|
||||
ID pgtype.UUID
|
||||
Name string
|
||||
@@ -976,6 +1016,33 @@ type Want struct {
|
||||
FulfilledAt pgtype.Timestamptz
|
||||
CreatedAt pgtype.Timestamptz
|
||||
UpdatedAt pgtype.Timestamptz
|
||||
SearchedAt pgtype.Timestamptz
|
||||
SearchOutcome string
|
||||
SearchConsidered int32
|
||||
BestRelease string
|
||||
BestVerdict string
|
||||
SearchError string
|
||||
}
|
||||
|
||||
type WantCandidate 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
|
||||
SearchedAt pgtype.Timestamptz
|
||||
}
|
||||
|
||||
type YouMightLikeAlbum struct {
|
||||
|
||||
@@ -212,7 +212,7 @@ ON CONFLICT (release_group_mbid) DO UPDATE
|
||||
fulfilled_at = NULL,
|
||||
updated_at = now()
|
||||
WHERE wants.fulfilled_at IS NOT NULL
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error
|
||||
`
|
||||
|
||||
type WantMissingAlbumParams struct {
|
||||
@@ -249,6 +249,12 @@ func (q *Queries) WantMissingAlbum(ctx context.Context, arg WantMissingAlbumPara
|
||||
&i.FulfilledAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SearchedAt,
|
||||
&i.SearchOutcome,
|
||||
&i.SearchConsidered,
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,585 @@
|
||||
// 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)
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6)
|
||||
ON CONFLICT (info_hash) DO UPDATE
|
||||
SET reason = EXCLUDED.reason, detail = EXCLUDED.detail, blocked_at = now()
|
||||
`
|
||||
|
||||
type BlockReleaseParams struct {
|
||||
InfoHash string
|
||||
Name string
|
||||
Reason string
|
||||
Detail string
|
||||
WantID pgtype.UUID
|
||||
BlockedBy pgtype.UUID
|
||||
}
|
||||
|
||||
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,
|
||||
)
|
||||
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 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,
|
||||
)
|
||||
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 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,
|
||||
)
|
||||
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_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
|
||||
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.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
|
||||
`
|
||||
|
||||
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,
|
||||
)
|
||||
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 FROM release_blocks ORDER BY blocked_at DESC
|
||||
`
|
||||
|
||||
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,
|
||||
); 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,
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING id, auto_grab, search_interval_hours, allowed_qualities, last_pass_at, last_pass, updated_at
|
||||
`
|
||||
|
||||
type UpdateAcquisitionSettingsParams struct {
|
||||
AutoGrab bool
|
||||
SearchIntervalHours int32
|
||||
AllowedQualities []string
|
||||
}
|
||||
|
||||
func (q *Queries) UpdateAcquisitionSettings(ctx context.Context, arg UpdateAcquisitionSettingsParams) (AcquisitionSetting, error) {
|
||||
row := q.db.QueryRow(ctx, updateAcquisitionSettings, arg.AutoGrab, arg.SearchIntervalHours, arg.AllowedQualities)
|
||||
var i AcquisitionSetting
|
||||
err := row.Scan(
|
||||
&i.ID,
|
||||
&i.AutoGrab,
|
||||
&i.SearchIntervalHours,
|
||||
&i.AllowedQualities,
|
||||
&i.LastPassAt,
|
||||
&i.LastPass,
|
||||
&i.UpdatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -206,7 +206,7 @@ func (q *Queries) GetReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (Re
|
||||
}
|
||||
|
||||
const getWantByReleaseGroup = `-- name: GetWantByReleaseGroup :one
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at FROM wants WHERE release_group_mbid = $1
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error FROM wants WHERE release_group_mbid = $1
|
||||
`
|
||||
|
||||
func (q *Queries) GetWantByReleaseGroup(ctx context.Context, releaseGroupMbid string) (Want, error) {
|
||||
@@ -227,6 +227,12 @@ func (q *Queries) GetWantByReleaseGroup(ctx context.Context, releaseGroupMbid st
|
||||
&i.FulfilledAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SearchedAt,
|
||||
&i.SearchOutcome,
|
||||
&i.SearchConsidered,
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
@@ -406,7 +412,7 @@ func (q *Queries) ListFollowsDue(ctx context.Context, arg ListFollowsDueParams)
|
||||
}
|
||||
|
||||
const listOpenWants = `-- name: ListOpenWants :many
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at FROM wants
|
||||
SELECT id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error FROM wants
|
||||
WHERE fulfilled_at IS NULL
|
||||
ORDER BY wanted_at, id
|
||||
LIMIT $1
|
||||
@@ -436,6 +442,12 @@ func (q *Queries) ListOpenWants(ctx context.Context, limit int32) ([]Want, error
|
||||
&i.FulfilledAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SearchedAt,
|
||||
&i.SearchOutcome,
|
||||
&i.SearchConsidered,
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -505,10 +517,14 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date,
|
||||
u.username AS requested_by,
|
||||
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id
|
||||
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id,
|
||||
-- The search schedule's last look (#5521), and the download running.
|
||||
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
|
||||
gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin, gr.quality AS grab_quality
|
||||
FROM wants w
|
||||
LEFT JOIN users u ON u.id = w.created_by
|
||||
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
|
||||
LEFT JOIN grabs gr ON gr.want_id = w.id AND gr.ended_at IS NULL
|
||||
WHERE CASE $1::text
|
||||
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
|
||||
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
|
||||
@@ -539,6 +555,16 @@ type ListWantsRow struct {
|
||||
FirstReleaseDate string
|
||||
RequestedBy *string
|
||||
LibraryArtistID pgtype.UUID
|
||||
SearchedAt pgtype.Timestamptz
|
||||
SearchOutcome string
|
||||
SearchConsidered int32
|
||||
BestRelease string
|
||||
BestVerdict string
|
||||
SearchError string
|
||||
GrabName *string
|
||||
GrabbedAt pgtype.Timestamptz
|
||||
GrabOrigin *string
|
||||
GrabQuality *string
|
||||
}
|
||||
|
||||
// The Wanted page: open wants (all but missing ones, or only missing ones),
|
||||
@@ -567,6 +593,16 @@ func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWan
|
||||
&i.FirstReleaseDate,
|
||||
&i.RequestedBy,
|
||||
&i.LibraryArtistID,
|
||||
&i.SearchedAt,
|
||||
&i.SearchOutcome,
|
||||
&i.SearchConsidered,
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
&i.GrabName,
|
||||
&i.GrabbedAt,
|
||||
&i.GrabOrigin,
|
||||
&i.GrabQuality,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -791,7 +827,7 @@ INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_
|
||||
VALUES ($1, $2, $3, $4,
|
||||
$5, $6, $7, $8)
|
||||
ON CONFLICT (release_group_mbid) DO UPDATE SET updated_at = wants.updated_at
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at
|
||||
RETURNING id, release_group_mbid, artist_mbid, artist_name, title, primary_type, first_release_date, reason, album_id, created_by, wanted_at, fulfilled_at, created_at, updated_at, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error
|
||||
`
|
||||
|
||||
type WantReleaseGroupParams struct {
|
||||
@@ -834,6 +870,12 @@ func (q *Queries) WantReleaseGroup(ctx context.Context, arg WantReleaseGroupPara
|
||||
&i.FulfilledAt,
|
||||
&i.CreatedAt,
|
||||
&i.UpdatedAt,
|
||||
&i.SearchedAt,
|
||||
&i.SearchOutcome,
|
||||
&i.SearchConsidered,
|
||||
&i.BestRelease,
|
||||
&i.BestVerdict,
|
||||
&i.SearchError,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
DROP TABLE release_blocks;
|
||||
DROP TABLE grabs;
|
||||
DROP TABLE want_candidates;
|
||||
DROP INDEX wants_search_due_idx;
|
||||
ALTER TABLE wants
|
||||
DROP COLUMN searched_at,
|
||||
DROP COLUMN search_outcome,
|
||||
DROP COLUMN search_considered,
|
||||
DROP COLUMN best_release,
|
||||
DROP COLUMN best_verdict,
|
||||
DROP COLUMN search_error;
|
||||
DROP TABLE acquisition_settings;
|
||||
@@ -0,0 +1,102 @@
|
||||
-- 0082_search_and_grab.up.sql — searching for wants and grabbing releases
|
||||
-- (Scribe milestone #502, #5521).
|
||||
--
|
||||
-- acquisition_settings one row: whether the schedule takes the best release
|
||||
-- by itself, how often a want is searched again, which
|
||||
-- quality tiers are taken, and the last pass's report.
|
||||
-- wants (columns) when each want was last searched and what came of it.
|
||||
-- want_candidates every release the last search found for a want, with
|
||||
-- its verdict and the reasons behind it.
|
||||
-- grabs downloads started for wants; at most one live per want.
|
||||
-- release_blocks releases never to take again, by infohash.
|
||||
|
||||
CREATE TABLE acquisition_settings (
|
||||
id boolean PRIMARY KEY DEFAULT true CONSTRAINT acquisition_settings_one_row CHECK (id),
|
||||
auto_grab boolean NOT NULL DEFAULT true,
|
||||
-- 0 switches the schedule off.
|
||||
search_interval_hours integer NOT NULL DEFAULT 24
|
||||
CONSTRAINT acquisition_settings_interval_check CHECK (search_interval_hours BETWEEN 0 AND 720),
|
||||
-- Lidarr's "Any" profile (note #5527): trash lossy to lossless; unknown off.
|
||||
allowed_qualities text[] NOT NULL DEFAULT ARRAY['lossless_hires', 'lossless', 'lossy_high',
|
||||
'lossy_standard', 'lossy_low', 'lossy_poor', 'lossy_trash'],
|
||||
last_pass_at timestamptz,
|
||||
last_pass jsonb NOT NULL DEFAULT '{}',
|
||||
updated_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
|
||||
INSERT INTO acquisition_settings DEFAULT VALUES;
|
||||
|
||||
ALTER TABLE wants
|
||||
ADD COLUMN searched_at timestamptz,
|
||||
ADD COLUMN search_outcome text NOT NULL DEFAULT ''
|
||||
CONSTRAINT wants_search_outcome_check CHECK (search_outcome IN ('', 'grabbed', 'waiting', 'nothing', 'error')),
|
||||
ADD COLUMN search_considered integer NOT NULL DEFAULT 0,
|
||||
ADD COLUMN best_release text NOT NULL DEFAULT '',
|
||||
ADD COLUMN best_verdict text NOT NULL DEFAULT '',
|
||||
ADD COLUMN search_error text NOT NULL DEFAULT '';
|
||||
|
||||
CREATE INDEX wants_search_due_idx ON wants (searched_at NULLS FIRST) WHERE fulfilled_at IS NULL;
|
||||
|
||||
CREATE TABLE want_candidates (
|
||||
want_id uuid NOT NULL REFERENCES wants (id) ON DELETE CASCADE,
|
||||
-- The infohash, or the name when the source gave none.
|
||||
key text NOT NULL,
|
||||
name text NOT NULL,
|
||||
info_hash text NOT NULL DEFAULT '',
|
||||
indexer text NOT NULL DEFAULT '',
|
||||
source text NOT NULL DEFAULT '',
|
||||
size_bytes bigint NOT NULL DEFAULT 0,
|
||||
seeders integer NOT NULL DEFAULT 0,
|
||||
files_count integer NOT NULL DEFAULT 0,
|
||||
format text NOT NULL DEFAULT '',
|
||||
quality text NOT NULL DEFAULT '',
|
||||
year integer NOT NULL DEFAULT 0,
|
||||
verdict text NOT NULL
|
||||
CONSTRAINT want_candidates_verdict_check CHECK (verdict IN ('grab', 'review', 'refused', 'rejected', 'blocked')),
|
||||
reason text NOT NULL DEFAULT '',
|
||||
reasons jsonb NOT NULL DEFAULT '[]',
|
||||
folded_from text[] NOT NULL DEFAULT '{}',
|
||||
-- Position in the search's order, best first.
|
||||
rank integer NOT NULL DEFAULT 0,
|
||||
searched_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (want_id, key)
|
||||
);
|
||||
|
||||
CREATE INDEX want_candidates_info_hash_idx ON want_candidates (info_hash) WHERE info_hash <> '';
|
||||
|
||||
CREATE TABLE grabs (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
want_id uuid REFERENCES wants (id) ON DELETE SET NULL,
|
||||
info_hash text NOT NULL DEFAULT '',
|
||||
name text NOT NULL,
|
||||
key text NOT NULL,
|
||||
indexer text NOT NULL DEFAULT '',
|
||||
source text NOT NULL DEFAULT '',
|
||||
-- The acquisition_services row of the download client it went to.
|
||||
client text NOT NULL,
|
||||
save_path text NOT NULL,
|
||||
quality text NOT NULL DEFAULT '',
|
||||
origin text NOT NULL CONSTRAINT grabs_origin_check CHECK (origin IN ('schedule', 'operator')),
|
||||
forced boolean NOT NULL DEFAULT false,
|
||||
grabbed_by uuid REFERENCES users (id) ON DELETE SET NULL,
|
||||
grabbed_at timestamptz NOT NULL DEFAULT now(),
|
||||
ended_at timestamptz,
|
||||
end_reason text NOT NULL DEFAULT ''
|
||||
CONSTRAINT grabs_end_reason_check CHECK (end_reason IN ('', 'superseded')),
|
||||
end_detail text NOT NULL DEFAULT ''
|
||||
);
|
||||
|
||||
-- One live grab per want; the service decides which, this is the backstop.
|
||||
CREATE UNIQUE INDEX grabs_one_live_per_want ON grabs (want_id) WHERE ended_at IS NULL AND want_id IS NOT NULL;
|
||||
CREATE INDEX grabs_info_hash_idx ON grabs (info_hash) WHERE info_hash <> '';
|
||||
|
||||
CREATE TABLE release_blocks (
|
||||
info_hash text PRIMARY KEY,
|
||||
name text NOT NULL DEFAULT '',
|
||||
-- operator: "never this one"; files: its .torrent held the wrong thing.
|
||||
reason text NOT NULL CONSTRAINT release_blocks_reason_check CHECK (reason IN ('operator', 'files')),
|
||||
detail text NOT NULL DEFAULT '',
|
||||
want_id uuid REFERENCES wants (id) ON DELETE SET NULL,
|
||||
blocked_by uuid REFERENCES users (id) ON DELETE SET NULL,
|
||||
blocked_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
@@ -0,0 +1,121 @@
|
||||
-- Searching for wants and grabbing releases (M502 #5521).
|
||||
|
||||
-- name: GetAcquisitionSettings :one
|
||||
SELECT * FROM acquisition_settings WHERE id = true;
|
||||
|
||||
-- name: UpdateAcquisitionSettings :one
|
||||
UPDATE acquisition_settings
|
||||
SET auto_grab = sqlc.arg(auto_grab),
|
||||
search_interval_hours = sqlc.arg(search_interval_hours),
|
||||
allowed_qualities = sqlc.arg(allowed_qualities),
|
||||
updated_at = now()
|
||||
WHERE id = true
|
||||
RETURNING *;
|
||||
|
||||
-- name: RecordSearchPass :exec
|
||||
UPDATE acquisition_settings SET last_pass_at = now(), last_pass = sqlc.arg(report) WHERE id = true;
|
||||
|
||||
-- name: ListWantsDueForSearch :many
|
||||
-- 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.
|
||||
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 => sqlc.arg(hours)::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 sqlc.arg(max_rows);
|
||||
|
||||
-- name: GetWantForSearch :one
|
||||
SELECT w.id, w.release_group_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;
|
||||
|
||||
-- name: EditionTrackSpan :one
|
||||
-- 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.
|
||||
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;
|
||||
|
||||
-- name: RecordWantSearch :exec
|
||||
UPDATE wants
|
||||
SET searched_at = now(),
|
||||
search_outcome = sqlc.arg(outcome),
|
||||
search_considered = sqlc.arg(considered),
|
||||
best_release = sqlc.arg(best_release),
|
||||
best_verdict = sqlc.arg(best_verdict),
|
||||
search_error = sqlc.arg(search_error),
|
||||
updated_at = now()
|
||||
WHERE id = sqlc.arg(id);
|
||||
|
||||
-- name: DeleteWantCandidates :exec
|
||||
DELETE FROM want_candidates WHERE want_id = $1;
|
||||
|
||||
-- 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 (sqlc.arg(want_id), sqlc.arg(key), sqlc.arg(name), sqlc.arg(info_hash), sqlc.arg(indexer),
|
||||
sqlc.arg(source), sqlc.arg(size_bytes), sqlc.arg(seeders), sqlc.arg(files_count), sqlc.arg(format),
|
||||
sqlc.arg(quality), sqlc.arg(year), sqlc.arg(verdict), sqlc.arg(reason), sqlc.arg(reasons),
|
||||
sqlc.arg(folded_from), sqlc.arg(rank))
|
||||
ON CONFLICT (want_id, key) DO NOTHING;
|
||||
|
||||
-- name: ListWantCandidates :many
|
||||
SELECT * FROM want_candidates WHERE want_id = $1 ORDER BY rank, key;
|
||||
|
||||
-- name: GetWantCandidate :one
|
||||
SELECT * FROM want_candidates WHERE want_id = $1 AND key = $2;
|
||||
|
||||
-- name: MarkCandidatesBlocked :exec
|
||||
UPDATE want_candidates
|
||||
SET verdict = 'blocked', reason = sqlc.arg(reason),
|
||||
reasons = jsonb_build_array(jsonb_build_object('kind', 'hard', 'code', 'blocked', 'text', sqlc.arg(reason)::text))
|
||||
WHERE info_hash = sqlc.arg(info_hash);
|
||||
|
||||
-- name: MarkCandidatesUnblocked :exec
|
||||
UPDATE want_candidates
|
||||
SET verdict = 'review', reason = sqlc.arg(reason),
|
||||
reasons = jsonb_build_array(jsonb_build_object('kind', 'soft', 'code', 'unblocked', 'text', sqlc.arg(reason)::text))
|
||||
WHERE info_hash = sqlc.arg(info_hash) AND verdict = 'blocked';
|
||||
|
||||
-- name: GetLiveGrab :one
|
||||
SELECT * FROM grabs WHERE want_id = $1 AND ended_at IS NULL;
|
||||
|
||||
-- name: InsertGrab :one
|
||||
INSERT INTO grabs (want_id, info_hash, name, key, indexer, source, client, save_path, quality,
|
||||
origin, forced, grabbed_by)
|
||||
VALUES (sqlc.narg(want_id), sqlc.arg(info_hash), sqlc.arg(name), sqlc.arg(key), sqlc.arg(indexer),
|
||||
sqlc.arg(source), sqlc.arg(client), sqlc.arg(save_path), sqlc.arg(quality),
|
||||
sqlc.arg(origin), sqlc.arg(forced), sqlc.narg(grabbed_by))
|
||||
RETURNING *;
|
||||
|
||||
-- name: EndGrab :exec
|
||||
UPDATE grabs
|
||||
SET ended_at = now(), end_reason = sqlc.arg(end_reason), end_detail = sqlc.arg(end_detail)
|
||||
WHERE id = sqlc.arg(id) AND ended_at IS NULL;
|
||||
|
||||
-- name: ListReleaseBlocks :many
|
||||
SELECT * FROM release_blocks ORDER BY blocked_at DESC;
|
||||
|
||||
-- name: BlockRelease :exec
|
||||
INSERT INTO release_blocks (info_hash, name, reason, detail, want_id, blocked_by)
|
||||
VALUES (sqlc.arg(info_hash), sqlc.arg(name), sqlc.arg(reason), sqlc.arg(detail),
|
||||
sqlc.narg(want_id), sqlc.narg(blocked_by))
|
||||
ON CONFLICT (info_hash) DO UPDATE
|
||||
SET reason = EXCLUDED.reason, detail = EXCLUDED.detail, blocked_at = now();
|
||||
|
||||
-- name: UnblockRelease :execrows
|
||||
DELETE FROM release_blocks WHERE info_hash = $1;
|
||||
@@ -186,10 +186,14 @@ SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reas
|
||||
COALESCE(g.secondary_types, '{}')::text[] AS secondary_types,
|
||||
COALESCE(NULLIF(w.first_release_date, ''), g.first_release_date, '')::text AS first_release_date,
|
||||
u.username AS requested_by,
|
||||
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id
|
||||
(SELECT ar.id FROM artists ar WHERE ar.mbid = w.artist_mbid) AS library_artist_id,
|
||||
-- The search schedule's last look (#5521), and the download running.
|
||||
w.searched_at, w.search_outcome, w.search_considered, w.best_release, w.best_verdict, w.search_error,
|
||||
gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin, gr.quality AS grab_quality
|
||||
FROM wants w
|
||||
LEFT JOIN users u ON u.id = w.created_by
|
||||
LEFT JOIN mb_release_groups g ON g.mbid = w.release_group_mbid
|
||||
LEFT JOIN grabs gr ON gr.want_id = w.id AND gr.ended_at IS NULL
|
||||
WHERE CASE sqlc.arg(state)::text
|
||||
WHEN 'held' THEN w.fulfilled_at IS NOT NULL
|
||||
WHEN 'missing' THEN w.fulfilled_at IS NULL AND w.reason = 'missing'
|
||||
|
||||
Reference in New Issue
Block a user