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
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>
920 lines
26 KiB
Go
920 lines
26 KiB
Go
// Code generated by sqlc. DO NOT EDIT.
|
|
// versions:
|
|
// sqlc v1.31.1
|
|
// source: wanted.sql
|
|
|
|
package dbq
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
const clearDefaultReleaseTypeProfile = `-- name: ClearDefaultReleaseTypeProfile :exec
|
|
UPDATE release_type_profiles SET is_default = false, updated_at = now() WHERE is_default
|
|
`
|
|
|
|
func (q *Queries) ClearDefaultReleaseTypeProfile(ctx context.Context) error {
|
|
_, err := q.db.Exec(ctx, clearDefaultReleaseTypeProfile)
|
|
return err
|
|
}
|
|
|
|
const countWants = `-- name: CountWants :one
|
|
SELECT COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason <> 'missing')::bigint AS wanted,
|
|
COUNT(*) FILTER (WHERE fulfilled_at IS NULL AND reason = 'missing')::bigint AS missing,
|
|
COUNT(*) FILTER (WHERE fulfilled_at IS NOT NULL)::bigint AS held,
|
|
(SELECT COUNT(*) FROM follows)::bigint AS following,
|
|
-- The Wanted view's other tabs (#5522): downloads running, those
|
|
-- waiting on a person, and releases set aside.
|
|
(SELECT COUNT(*) FROM grabs WHERE ended_at IS NULL)::bigint AS activity,
|
|
(SELECT COUNT(*) FROM grabs
|
|
WHERE ended_at IS NULL AND import_state IN ('review', 'failed'))::bigint AS attention,
|
|
(SELECT COUNT(*) FROM release_blocks b
|
|
WHERE b.expires_at IS NULL OR b.expires_at > now())::bigint AS blocked
|
|
FROM wants
|
|
`
|
|
|
|
type CountWantsRow struct {
|
|
Wanted int64
|
|
Missing int64
|
|
Held int64
|
|
Following int64
|
|
Activity int64
|
|
Attention int64
|
|
Blocked int64
|
|
}
|
|
|
|
func (q *Queries) CountWants(ctx context.Context) (CountWantsRow, error) {
|
|
row := q.db.QueryRow(ctx, countWants)
|
|
var i CountWantsRow
|
|
err := row.Scan(
|
|
&i.Wanted,
|
|
&i.Missing,
|
|
&i.Held,
|
|
&i.Following,
|
|
&i.Activity,
|
|
&i.Attention,
|
|
&i.Blocked,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const createReleaseTypeProfile = `-- name: CreateReleaseTypeProfile :one
|
|
INSERT INTO release_type_profiles (name, primary_types, secondary_types)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, name, primary_types, secondary_types, is_default, created_at, updated_at
|
|
`
|
|
|
|
type CreateReleaseTypeProfileParams struct {
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
}
|
|
|
|
func (q *Queries) CreateReleaseTypeProfile(ctx context.Context, arg CreateReleaseTypeProfileParams) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, createReleaseTypeProfile, arg.Name, arg.PrimaryTypes, arg.SecondaryTypes)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const deleteFollow = `-- name: DeleteFollow :execrows
|
|
DELETE FROM follows WHERE artist_mbid = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteFollow(ctx context.Context, artistMbid string) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteFollow, artistMbid)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deleteReleaseTypeProfile = `-- name: DeleteReleaseTypeProfile :execrows
|
|
DELETE FROM release_type_profiles WHERE id = $1 AND NOT is_default
|
|
`
|
|
|
|
// The default stays; one in use is refused by the follows foreign key.
|
|
func (q *Queries) DeleteReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteReleaseTypeProfile, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const deleteWant = `-- name: DeleteWant :execrows
|
|
DELETE FROM wants WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) DeleteWant(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, deleteWant, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const fulfillWant = `-- name: FulfillWant :exec
|
|
UPDATE wants
|
|
SET fulfilled_at = now(),
|
|
album_id = $1,
|
|
updated_at = now()
|
|
WHERE id = $2 AND fulfilled_at IS NULL
|
|
`
|
|
|
|
type FulfillWantParams struct {
|
|
AlbumID pgtype.UUID
|
|
ID pgtype.UUID
|
|
}
|
|
|
|
func (q *Queries) FulfillWant(ctx context.Context, arg FulfillWantParams) error {
|
|
_, err := q.db.Exec(ctx, fulfillWant, arg.AlbumID, arg.ID)
|
|
return err
|
|
}
|
|
|
|
const getDefaultReleaseTypeProfile = `-- name: GetDefaultReleaseTypeProfile :one
|
|
SELECT id, name, primary_types, secondary_types, is_default, created_at, updated_at FROM release_type_profiles WHERE is_default
|
|
`
|
|
|
|
func (q *Queries) GetDefaultReleaseTypeProfile(ctx context.Context) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getDefaultReleaseTypeProfile)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getFollow = `-- name: GetFollow :one
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_by, f.followed_at, f.baseline_at, f.want_existing, f.checked_at, f.updated_at, p.name AS profile_name
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
WHERE f.artist_mbid = $1
|
|
`
|
|
|
|
type GetFollowRow struct {
|
|
ArtistMbid string
|
|
ArtistName string
|
|
ProfileID pgtype.UUID
|
|
FollowedBy pgtype.UUID
|
|
FollowedAt pgtype.Timestamptz
|
|
BaselineAt pgtype.Timestamptz
|
|
WantExisting bool
|
|
CheckedAt pgtype.Timestamptz
|
|
UpdatedAt pgtype.Timestamptz
|
|
ProfileName string
|
|
}
|
|
|
|
func (q *Queries) GetFollow(ctx context.Context, artistMbid string) (GetFollowRow, error) {
|
|
row := q.db.QueryRow(ctx, getFollow, artistMbid)
|
|
var i GetFollowRow
|
|
err := row.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
&i.ProfileName,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const getReleaseTypeProfile = `-- name: GetReleaseTypeProfile :one
|
|
SELECT id, name, primary_types, secondary_types, is_default, created_at, updated_at FROM release_type_profiles WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) GetReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, getReleaseTypeProfile, id)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
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, 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) {
|
|
row := q.db.QueryRow(ctx, getWantByReleaseGroup, releaseGroupMbid)
|
|
var i Want
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const libraryHoldsReleaseGroup = `-- name: LibraryHoldsReleaseGroup :one
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM albums a
|
|
WHERE (a.release_group_mbid = $1::text
|
|
OR a.mbid = $1
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = $1)
|
|
OR (lower(a.title) = lower($2::text)
|
|
AND a.artist_id IN (SELECT ar.id FROM artists ar WHERE ar.mbid = $3::text)))
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id AND t.missing_since IS NULL)
|
|
)
|
|
`
|
|
|
|
type LibraryHoldsReleaseGroupParams struct {
|
|
GroupMbid string
|
|
Title string
|
|
ArtistMbid string
|
|
}
|
|
|
|
// Whether the library already has the group: an album tagged with it or
|
|
// with one of its releases, or failing that an album of the artist's with
|
|
// the same title. Only albums with a track on disk count.
|
|
func (q *Queries) LibraryHoldsReleaseGroup(ctx context.Context, arg LibraryHoldsReleaseGroupParams) (bool, error) {
|
|
row := q.db.QueryRow(ctx, libraryHoldsReleaseGroup, arg.GroupMbid, arg.Title, arg.ArtistMbid)
|
|
var exists bool
|
|
err := row.Scan(&exists)
|
|
return exists, err
|
|
}
|
|
|
|
const listFollowGroups = `-- name: ListFollowGroups :many
|
|
SELECT g.mbid, g.title, g.primary_type, g.secondary_types, g.first_release_date,
|
|
EXISTS (SELECT 1 FROM follow_seen_groups s
|
|
WHERE s.artist_mbid = $1 AND s.release_group_mbid = g.mbid) AS seen
|
|
FROM mb_artist_release_groups ag
|
|
JOIN mb_release_groups g ON g.mbid = ag.release_group_mbid
|
|
WHERE ag.artist_mbid = $1
|
|
ORDER BY g.first_release_date, g.title
|
|
`
|
|
|
|
type ListFollowGroupsRow struct {
|
|
Mbid string
|
|
Title string
|
|
PrimaryType string
|
|
SecondaryTypes []string
|
|
FirstReleaseDate string
|
|
Seen bool
|
|
}
|
|
|
|
// An artist's release groups from the catalogue, each with whether the
|
|
// follow has judged it before.
|
|
func (q *Queries) ListFollowGroups(ctx context.Context, artistMbid string) ([]ListFollowGroupsRow, error) {
|
|
rows, err := q.db.Query(ctx, listFollowGroups, artistMbid)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListFollowGroupsRow
|
|
for rows.Next() {
|
|
var i ListFollowGroupsRow
|
|
if err := rows.Scan(
|
|
&i.Mbid,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.SecondaryTypes,
|
|
&i.FirstReleaseDate,
|
|
&i.Seen,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFollows = `-- name: ListFollows :many
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_at, f.baseline_at, f.checked_at,
|
|
p.name AS profile_name,
|
|
(SELECT a.id FROM artists a WHERE a.mbid = f.artist_mbid) AS artist_id,
|
|
(SELECT COUNT(*) FROM wants w
|
|
WHERE w.artist_mbid = f.artist_mbid AND w.fulfilled_at IS NULL)::bigint AS open_wants
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
ORDER BY lower(f.artist_name), f.artist_mbid
|
|
`
|
|
|
|
type ListFollowsRow struct {
|
|
ArtistMbid string
|
|
ArtistName string
|
|
ProfileID pgtype.UUID
|
|
FollowedAt pgtype.Timestamptz
|
|
BaselineAt pgtype.Timestamptz
|
|
CheckedAt pgtype.Timestamptz
|
|
ProfileName string
|
|
ArtistID pgtype.UUID
|
|
OpenWants int64
|
|
}
|
|
|
|
func (q *Queries) ListFollows(ctx context.Context) ([]ListFollowsRow, error) {
|
|
rows, err := q.db.Query(ctx, listFollows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListFollowsRow
|
|
for rows.Next() {
|
|
var i ListFollowsRow
|
|
if err := rows.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.CheckedAt,
|
|
&i.ProfileName,
|
|
&i.ArtistID,
|
|
&i.OpenWants,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listFollowsDue = `-- name: ListFollowsDue :many
|
|
SELECT f.artist_mbid, f.artist_name, f.profile_id, f.followed_by, f.followed_at, f.baseline_at, f.want_existing, f.checked_at, f.updated_at
|
|
FROM follows f
|
|
WHERE f.baseline_at IS NULL OR f.checked_at IS NULL OR f.checked_at < $1
|
|
ORDER BY f.baseline_at NULLS FIRST, f.checked_at NULLS FIRST, f.artist_mbid
|
|
LIMIT $2
|
|
`
|
|
|
|
type ListFollowsDueParams struct {
|
|
Before pgtype.Timestamptz
|
|
MaxRows int32
|
|
}
|
|
|
|
// Follows whose discography has not been applied since @before, never-read
|
|
// first, so a new follow's baseline is not stuck behind daily re-checks.
|
|
func (q *Queries) ListFollowsDue(ctx context.Context, arg ListFollowsDueParams) ([]Follow, error) {
|
|
rows, err := q.db.Query(ctx, listFollowsDue, arg.Before, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Follow
|
|
for rows.Next() {
|
|
var i Follow
|
|
if err := rows.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
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, 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
|
|
`
|
|
|
|
func (q *Queries) ListOpenWants(ctx context.Context, limit int32) ([]Want, error) {
|
|
rows, err := q.db.Query(ctx, listOpenWants, limit)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []Want
|
|
for rows.Next() {
|
|
var i Want
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listReleaseTypeProfiles = `-- name: ListReleaseTypeProfiles :many
|
|
|
|
SELECT p.id, p.name, p.primary_types, p.secondary_types, p.is_default, p.created_at, p.updated_at,
|
|
(SELECT COUNT(*) FROM follows f WHERE f.profile_id = p.id)::bigint AS follow_count
|
|
FROM release_type_profiles p
|
|
ORDER BY p.is_default DESC, p.name
|
|
`
|
|
|
|
type ListReleaseTypeProfilesRow struct {
|
|
ID pgtype.UUID
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
IsDefault bool
|
|
CreatedAt pgtype.Timestamptz
|
|
UpdatedAt pgtype.Timestamptz
|
|
FollowCount int64
|
|
}
|
|
|
|
// The wanted list (M502 #5520): release-type profiles, follows and wants.
|
|
func (q *Queries) ListReleaseTypeProfiles(ctx context.Context) ([]ListReleaseTypeProfilesRow, error) {
|
|
rows, err := q.db.Query(ctx, listReleaseTypeProfiles)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListReleaseTypeProfilesRow
|
|
for rows.Next() {
|
|
var i ListReleaseTypeProfilesRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.FollowCount,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const listWants = `-- name: ListWants :many
|
|
SELECT w.id, w.release_group_mbid, w.artist_mbid, w.artist_name, w.title, w.reason,
|
|
w.album_id, w.wanted_at, w.fulfilled_at,
|
|
-- A request names an album before the catalogue has read it: the
|
|
-- catalogue's type and date fill in once it has.
|
|
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,
|
|
u.username AS requested_by,
|
|
(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,
|
|
-- Its latest download, running or ended, and what the import made of
|
|
-- it (#5522).
|
|
gr.id AS grab_id, gr.name AS grab_name, gr.grabbed_at, gr.origin AS grab_origin,
|
|
gr.quality AS grab_quality, gr.ended_at AS grab_ended_at, gr.end_reason AS grab_end_reason,
|
|
gr.progress AS grab_progress, gr.import_state AS grab_import_state,
|
|
gr.import_reason AS grab_import_reason, gr.end_detail AS grab_end_detail, gr.imported_at AS grab_imported_at,
|
|
gr.imported_files AS grab_imported_files
|
|
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.id = (SELECT x.id FROM grabs x WHERE x.want_id = w.id
|
|
ORDER BY x.ended_at IS NULL DESC, x.grabbed_at DESC LIMIT 1)
|
|
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'
|
|
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
|
|
END
|
|
ORDER BY COALESCE(w.fulfilled_at, w.wanted_at) DESC, w.id
|
|
LIMIT $3 OFFSET $2
|
|
`
|
|
|
|
type ListWantsParams struct {
|
|
State string
|
|
Skip int32
|
|
MaxRows int32
|
|
}
|
|
|
|
type ListWantsRow struct {
|
|
ID pgtype.UUID
|
|
ReleaseGroupMbid string
|
|
ArtistMbid string
|
|
ArtistName string
|
|
Title string
|
|
Reason string
|
|
AlbumID pgtype.UUID
|
|
WantedAt pgtype.Timestamptz
|
|
FulfilledAt pgtype.Timestamptz
|
|
PrimaryType string
|
|
SecondaryTypes []string
|
|
FirstReleaseDate string
|
|
RequestedBy *string
|
|
LibraryArtistID pgtype.UUID
|
|
SearchedAt pgtype.Timestamptz
|
|
SearchOutcome string
|
|
SearchConsidered int32
|
|
BestRelease string
|
|
BestVerdict string
|
|
SearchError string
|
|
GrabID pgtype.UUID
|
|
GrabName *string
|
|
GrabbedAt pgtype.Timestamptz
|
|
GrabOrigin *string
|
|
GrabQuality *string
|
|
GrabEndedAt pgtype.Timestamptz
|
|
GrabEndReason *string
|
|
GrabProgress *float32
|
|
GrabImportState *string
|
|
GrabImportReason *string
|
|
GrabEndDetail *string
|
|
GrabImportedAt pgtype.Timestamptz
|
|
GrabImportedFiles *int32
|
|
}
|
|
|
|
// The Wanted page: open wants (all but missing ones, or only missing ones),
|
|
// or what was held most recently.
|
|
func (q *Queries) ListWants(ctx context.Context, arg ListWantsParams) ([]ListWantsRow, error) {
|
|
rows, err := q.db.Query(ctx, listWants, arg.State, arg.Skip, arg.MaxRows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []ListWantsRow
|
|
for rows.Next() {
|
|
var i ListWantsRow
|
|
if err := rows.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.PrimaryType,
|
|
&i.SecondaryTypes,
|
|
&i.FirstReleaseDate,
|
|
&i.RequestedBy,
|
|
&i.LibraryArtistID,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
&i.GrabID,
|
|
&i.GrabName,
|
|
&i.GrabbedAt,
|
|
&i.GrabOrigin,
|
|
&i.GrabQuality,
|
|
&i.GrabEndedAt,
|
|
&i.GrabEndReason,
|
|
&i.GrabProgress,
|
|
&i.GrabImportState,
|
|
&i.GrabImportReason,
|
|
&i.GrabEndDetail,
|
|
&i.GrabImportedAt,
|
|
&i.GrabImportedFiles,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const markFollowChecked = `-- name: MarkFollowChecked :exec
|
|
UPDATE follows
|
|
SET checked_at = now(),
|
|
baseline_at = COALESCE(baseline_at, now()),
|
|
want_existing = false,
|
|
updated_at = now()
|
|
WHERE artist_mbid = $1
|
|
`
|
|
|
|
func (q *Queries) MarkFollowChecked(ctx context.Context, artistMbid string) error {
|
|
_, err := q.db.Exec(ctx, markFollowChecked, artistMbid)
|
|
return err
|
|
}
|
|
|
|
const markGroupSeen = `-- name: MarkGroupSeen :exec
|
|
INSERT INTO follow_seen_groups (artist_mbid, release_group_mbid)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING
|
|
`
|
|
|
|
type MarkGroupSeenParams struct {
|
|
ArtistMbid string
|
|
ReleaseGroupMbid string
|
|
}
|
|
|
|
func (q *Queries) MarkGroupSeen(ctx context.Context, arg MarkGroupSeenParams) error {
|
|
_, err := q.db.Exec(ctx, markGroupSeen, arg.ArtistMbid, arg.ReleaseGroupMbid)
|
|
return err
|
|
}
|
|
|
|
const repointWant = `-- name: RepointWant :exec
|
|
UPDATE wants SET release_group_mbid = $2, updated_at = now() WHERE id = $1
|
|
`
|
|
|
|
type RepointWantParams struct {
|
|
ID pgtype.UUID
|
|
ReleaseGroupMbid string
|
|
}
|
|
|
|
// A want recorded under a release id before #5241 is moved to its group.
|
|
func (q *Queries) RepointWant(ctx context.Context, arg RepointWantParams) error {
|
|
_, err := q.db.Exec(ctx, repointWant, arg.ID, arg.ReleaseGroupMbid)
|
|
return err
|
|
}
|
|
|
|
const setDefaultReleaseTypeProfile = `-- name: SetDefaultReleaseTypeProfile :execrows
|
|
UPDATE release_type_profiles SET is_default = true, updated_at = now() WHERE id = $1
|
|
`
|
|
|
|
func (q *Queries) SetDefaultReleaseTypeProfile(ctx context.Context, id pgtype.UUID) (int64, error) {
|
|
result, err := q.db.Exec(ctx, setDefaultReleaseTypeProfile, id)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
return result.RowsAffected(), nil
|
|
}
|
|
|
|
const updateReleaseTypeProfile = `-- name: UpdateReleaseTypeProfile :one
|
|
UPDATE release_type_profiles
|
|
SET name = $2,
|
|
primary_types = $3,
|
|
secondary_types = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING id, name, primary_types, secondary_types, is_default, created_at, updated_at
|
|
`
|
|
|
|
type UpdateReleaseTypeProfileParams struct {
|
|
ID pgtype.UUID
|
|
Name string
|
|
PrimaryTypes []string
|
|
SecondaryTypes []string
|
|
}
|
|
|
|
func (q *Queries) UpdateReleaseTypeProfile(ctx context.Context, arg UpdateReleaseTypeProfileParams) (ReleaseTypeProfile, error) {
|
|
row := q.db.QueryRow(ctx, updateReleaseTypeProfile,
|
|
arg.ID,
|
|
arg.Name,
|
|
arg.PrimaryTypes,
|
|
arg.SecondaryTypes,
|
|
)
|
|
var i ReleaseTypeProfile
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.Name,
|
|
&i.PrimaryTypes,
|
|
&i.SecondaryTypes,
|
|
&i.IsDefault,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const upsertFollow = `-- name: UpsertFollow :one
|
|
INSERT INTO follows (artist_mbid, artist_name, profile_id, followed_by, want_existing)
|
|
VALUES ($1, $2, $3,
|
|
$4, $5)
|
|
ON CONFLICT (artist_mbid) DO UPDATE
|
|
SET artist_name = EXCLUDED.artist_name,
|
|
profile_id = EXCLUDED.profile_id,
|
|
want_existing = follows.want_existing OR EXCLUDED.want_existing,
|
|
baseline_at = CASE WHEN EXCLUDED.want_existing THEN NULL ELSE follows.baseline_at END,
|
|
updated_at = now()
|
|
RETURNING artist_mbid, artist_name, profile_id, followed_by, followed_at, baseline_at, want_existing, checked_at, updated_at
|
|
`
|
|
|
|
type UpsertFollowParams struct {
|
|
ArtistMbid string
|
|
ArtistName string
|
|
ProfileID pgtype.UUID
|
|
FollowedBy pgtype.UUID
|
|
WantExisting bool
|
|
}
|
|
|
|
// Following an artist already followed changes its profile. want_existing
|
|
// only ever turns on here; the worker turns it off once applied, and turning
|
|
// it on again re-reads the baseline so the existing releases are judged.
|
|
func (q *Queries) UpsertFollow(ctx context.Context, arg UpsertFollowParams) (Follow, error) {
|
|
row := q.db.QueryRow(ctx, upsertFollow,
|
|
arg.ArtistMbid,
|
|
arg.ArtistName,
|
|
arg.ProfileID,
|
|
arg.FollowedBy,
|
|
arg.WantExisting,
|
|
)
|
|
var i Follow
|
|
err := row.Scan(
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.ProfileID,
|
|
&i.FollowedBy,
|
|
&i.FollowedAt,
|
|
&i.BaselineAt,
|
|
&i.WantExisting,
|
|
&i.CheckedAt,
|
|
&i.UpdatedAt,
|
|
)
|
|
return i, err
|
|
}
|
|
|
|
const wantHoldings = `-- name: WantHoldings :many
|
|
SELECT a.id AS album_id,
|
|
COUNT(*) FILTER (WHERE t.missing_since IS NULL)::int AS present,
|
|
COALESCE(bool_or(t.missing_since IS NULL AND t.added_at > $1::timestamptz), false)::bool AS arrived_since,
|
|
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < $1::timestamptz), false)::bool AS missing_before,
|
|
-- Missing tracks no track on disk stands in for, by position or
|
|
-- title. A missing row whose song is back at a new path (#5245) is
|
|
-- covered; a lost bonus track is not.
|
|
COUNT(*) FILTER (WHERE t.missing_since IS NOT NULL AND NOT EXISTS (
|
|
SELECT 1 FROM tracks p
|
|
WHERE p.album_id = a.id AND p.missing_since IS NULL
|
|
AND ((p.track_number = t.track_number AND p.disc_number IS NOT DISTINCT FROM t.disc_number)
|
|
OR lower(p.title) = lower(t.title))))::int AS uncovered_missing
|
|
FROM albums a
|
|
JOIN tracks t ON t.album_id = a.id
|
|
WHERE a.release_group_mbid = $2::text
|
|
OR a.mbid = $2
|
|
OR a.id = $3
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = $2)
|
|
GROUP BY a.id
|
|
ORDER BY present DESC, a.id
|
|
`
|
|
|
|
type WantHoldingsParams struct {
|
|
Since pgtype.Timestamptz
|
|
GroupMbid string
|
|
AlbumID pgtype.UUID
|
|
}
|
|
|
|
type WantHoldingsRow struct {
|
|
AlbumID pgtype.UUID
|
|
Present int32
|
|
ArrivedSince bool
|
|
MissingBefore bool
|
|
UncoveredMissing int32
|
|
}
|
|
|
|
// Each library album that may answer a want, with what it has on disk:
|
|
// tracks present, whether one arrived since the want, and whether a track
|
|
// missing before the want is still missing.
|
|
func (q *Queries) WantHoldings(ctx context.Context, arg WantHoldingsParams) ([]WantHoldingsRow, error) {
|
|
rows, err := q.db.Query(ctx, wantHoldings, arg.Since, arg.GroupMbid, arg.AlbumID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var items []WantHoldingsRow
|
|
for rows.Next() {
|
|
var i WantHoldingsRow
|
|
if err := rows.Scan(
|
|
&i.AlbumID,
|
|
&i.Present,
|
|
&i.ArrivedSince,
|
|
&i.MissingBefore,
|
|
&i.UncoveredMissing,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
items = append(items, i)
|
|
}
|
|
if err := rows.Err(); err != nil {
|
|
return nil, err
|
|
}
|
|
return items, nil
|
|
}
|
|
|
|
const wantReleaseGroup = `-- name: WantReleaseGroup :one
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type,
|
|
first_release_date, reason, created_by)
|
|
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, searched_at, search_outcome, search_considered, best_release, best_verdict, search_error
|
|
`
|
|
|
|
type WantReleaseGroupParams struct {
|
|
ReleaseGroupMbid string
|
|
ArtistMbid string
|
|
ArtistName string
|
|
Title string
|
|
PrimaryType string
|
|
FirstReleaseDate string
|
|
Reason string
|
|
CreatedBy pgtype.UUID
|
|
}
|
|
|
|
// Wants a release group, or returns the want already recorded for it. A
|
|
// held want is not reopened here: the library has it.
|
|
func (q *Queries) WantReleaseGroup(ctx context.Context, arg WantReleaseGroupParams) (Want, error) {
|
|
row := q.db.QueryRow(ctx, wantReleaseGroup,
|
|
arg.ReleaseGroupMbid,
|
|
arg.ArtistMbid,
|
|
arg.ArtistName,
|
|
arg.Title,
|
|
arg.PrimaryType,
|
|
arg.FirstReleaseDate,
|
|
arg.Reason,
|
|
arg.CreatedBy,
|
|
)
|
|
var i Want
|
|
err := row.Scan(
|
|
&i.ID,
|
|
&i.ReleaseGroupMbid,
|
|
&i.ArtistMbid,
|
|
&i.ArtistName,
|
|
&i.Title,
|
|
&i.PrimaryType,
|
|
&i.FirstReleaseDate,
|
|
&i.Reason,
|
|
&i.AlbumID,
|
|
&i.CreatedBy,
|
|
&i.WantedAt,
|
|
&i.FulfilledAt,
|
|
&i.CreatedAt,
|
|
&i.UpdatedAt,
|
|
&i.SearchedAt,
|
|
&i.SearchOutcome,
|
|
&i.SearchConsidered,
|
|
&i.BestRelease,
|
|
&i.BestVerdict,
|
|
&i.SearchError,
|
|
)
|
|
return i, err
|
|
}
|