release / govulncheck (push) Successful in 15s
release / web (push) Failing after 1m21s
release / go (push) Successful in 1m35s
release / integration (push) Failing after 4m57s
release / android (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m28s
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
Minstrel keeps its own list of what it is to get, keyed by MusicBrainz release group. A want comes from an approved request, a new release by a followed artist, or an album whose files went missing, and is held once the library has a full edition of it. - Migration 0081: release_type_profiles (Standard seeded: Album, EP, Single, studio only), follows, follow_seen_groups and wants. lidarr_requests becomes requests with plain *_mbid columns; approved album and track requests move onto the wanted list, approved artist requests become follows, and the library's album artists start out followed. missing_reacquisitions and the retry settings go. - internal/wanted: follows are checked against MusicBrainz discographies daily. The first check sets a baseline, and only releases after it are wanted unless the follow asked for what already exists. Profiles filter by primary and secondary type, so live recordings stay out unless a profile ticks Live. - internal/requests (was lidarrrequests): approving puts the request on the wanted list, and an artist request can be approved on a chosen profile. A request completes when its want is held. - The re-acquisition sweeper wants a missing album back instead of asking Lidarr; searching for it is the search schedule's (#5521). - Web: an admin Wanted page (Wanted, Missing files, Held, Following), a follow control on artist pages, a release-type profiles card on Integrations, "Approve on a profile" on Requests, and want state on Missing files. The artist detail API returns the artist's MBID. - Android: the request body sends artist_mbid, album_mbid and track_mbid. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
207 lines
9.1 KiB
SQL
207 lines
9.1 KiB
SQL
-- The wanted list (M502 #5520): release-type profiles, follows and wants.
|
|
|
|
-- name: ListReleaseTypeProfiles :many
|
|
SELECT p.*,
|
|
(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;
|
|
|
|
-- name: GetReleaseTypeProfile :one
|
|
SELECT * FROM release_type_profiles WHERE id = $1;
|
|
|
|
-- name: GetDefaultReleaseTypeProfile :one
|
|
SELECT * FROM release_type_profiles WHERE is_default;
|
|
|
|
-- name: CreateReleaseTypeProfile :one
|
|
INSERT INTO release_type_profiles (name, primary_types, secondary_types)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING *;
|
|
|
|
-- name: UpdateReleaseTypeProfile :one
|
|
UPDATE release_type_profiles
|
|
SET name = $2,
|
|
primary_types = $3,
|
|
secondary_types = $4,
|
|
updated_at = now()
|
|
WHERE id = $1
|
|
RETURNING *;
|
|
|
|
-- name: ClearDefaultReleaseTypeProfile :exec
|
|
UPDATE release_type_profiles SET is_default = false, updated_at = now() WHERE is_default;
|
|
|
|
-- name: SetDefaultReleaseTypeProfile :execrows
|
|
UPDATE release_type_profiles SET is_default = true, updated_at = now() WHERE id = $1;
|
|
|
|
-- name: DeleteReleaseTypeProfile :execrows
|
|
-- The default stays; one in use is refused by the follows foreign key.
|
|
DELETE FROM release_type_profiles WHERE id = $1 AND NOT is_default;
|
|
|
|
-- name: UpsertFollow :one
|
|
-- 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.
|
|
INSERT INTO follows (artist_mbid, artist_name, profile_id, followed_by, want_existing)
|
|
VALUES (sqlc.arg(artist_mbid), sqlc.arg(artist_name), sqlc.arg(profile_id),
|
|
sqlc.narg(followed_by), sqlc.arg(want_existing))
|
|
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 *;
|
|
|
|
-- name: GetFollow :one
|
|
SELECT f.*, p.name AS profile_name
|
|
FROM follows f
|
|
JOIN release_type_profiles p ON p.id = f.profile_id
|
|
WHERE f.artist_mbid = $1;
|
|
|
|
-- name: DeleteFollow :execrows
|
|
DELETE FROM follows WHERE artist_mbid = $1;
|
|
|
|
-- 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;
|
|
|
|
-- name: ListFollowsDue :many
|
|
-- 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.
|
|
SELECT f.*
|
|
FROM follows f
|
|
WHERE f.baseline_at IS NULL OR f.checked_at IS NULL OR f.checked_at < sqlc.arg(before)
|
|
ORDER BY f.baseline_at NULLS FIRST, f.checked_at NULLS FIRST, f.artist_mbid
|
|
LIMIT sqlc.arg(max_rows);
|
|
|
|
-- 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;
|
|
|
|
-- name: ListFollowGroups :many
|
|
-- An artist's release groups from the catalogue, each with whether the
|
|
-- follow has judged it before.
|
|
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 = sqlc.arg(artist_mbid) 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 = sqlc.arg(artist_mbid)
|
|
ORDER BY g.first_release_date, g.title;
|
|
|
|
-- name: MarkGroupSeen :exec
|
|
INSERT INTO follow_seen_groups (artist_mbid, release_group_mbid)
|
|
VALUES ($1, $2)
|
|
ON CONFLICT DO NOTHING;
|
|
|
|
-- name: LibraryHoldsReleaseGroup :one
|
|
-- 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.
|
|
SELECT EXISTS (
|
|
SELECT 1 FROM albums a
|
|
WHERE (a.release_group_mbid = sqlc.arg(group_mbid)::text
|
|
OR a.mbid = sqlc.arg(group_mbid)
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = sqlc.arg(group_mbid))
|
|
OR (lower(a.title) = lower(sqlc.arg(title)::text)
|
|
AND a.artist_id IN (SELECT ar.id FROM artists ar WHERE ar.mbid = sqlc.arg(artist_mbid)::text)))
|
|
AND EXISTS (SELECT 1 FROM tracks t WHERE t.album_id = a.id AND t.missing_since IS NULL)
|
|
);
|
|
|
|
-- name: WantReleaseGroup :one
|
|
-- Wants a release group, or returns the want already recorded for it. A
|
|
-- held want is not reopened here: the library has it.
|
|
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, primary_type,
|
|
first_release_date, reason, created_by)
|
|
VALUES (sqlc.arg(release_group_mbid), sqlc.arg(artist_mbid), sqlc.arg(artist_name), sqlc.arg(title),
|
|
sqlc.arg(primary_type), sqlc.arg(first_release_date), sqlc.arg(reason), sqlc.narg(created_by))
|
|
ON CONFLICT (release_group_mbid) DO UPDATE SET updated_at = wants.updated_at
|
|
RETURNING *;
|
|
|
|
-- name: GetWantByReleaseGroup :one
|
|
SELECT * FROM wants WHERE release_group_mbid = $1;
|
|
|
|
-- name: ListOpenWants :many
|
|
SELECT * FROM wants
|
|
WHERE fulfilled_at IS NULL
|
|
ORDER BY wanted_at, id
|
|
LIMIT $1;
|
|
|
|
-- name: WantHoldings :many
|
|
-- 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.
|
|
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 > sqlc.arg(since)::timestamptz), false)::bool AS arrived_since,
|
|
COALESCE(bool_or(t.missing_since IS NOT NULL AND t.missing_since < sqlc.arg(since)::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 = sqlc.arg(group_mbid)::text
|
|
OR a.mbid = sqlc.arg(group_mbid)
|
|
OR a.id = sqlc.narg(album_id)
|
|
OR a.mbid IN (SELECT r.mbid FROM mb_releases r WHERE r.release_group_mbid = sqlc.arg(group_mbid))
|
|
GROUP BY a.id
|
|
ORDER BY present DESC, a.id;
|
|
|
|
-- name: FulfillWant :exec
|
|
UPDATE wants
|
|
SET fulfilled_at = now(),
|
|
album_id = sqlc.arg(album_id),
|
|
updated_at = now()
|
|
WHERE id = sqlc.arg(id) AND fulfilled_at IS NULL;
|
|
|
|
-- name: RepointWant :exec
|
|
-- A want recorded under a release id before #5241 is moved to its group.
|
|
UPDATE wants SET release_group_mbid = $2, updated_at = now() WHERE id = $1;
|
|
|
|
-- name: DeleteWant :execrows
|
|
DELETE FROM wants WHERE id = $1;
|
|
|
|
-- name: ListWants :many
|
|
-- The Wanted page: open wants (all but missing ones, or only missing ones),
|
|
-- or what was held most recently.
|
|
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
|
|
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
|
|
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'
|
|
ELSE w.fulfilled_at IS NULL AND w.reason <> 'missing'
|
|
END
|
|
ORDER BY COALESCE(w.fulfilled_at, w.wanted_at) DESC, w.id
|
|
LIMIT sqlc.arg(max_rows) OFFSET sqlc.arg(skip);
|
|
|
|
-- 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
|
|
FROM wants;
|