Files
minstrel/internal/db/migrations/0081_wanted.up.sql
T
bvandeusenandClaude Opus 5.5 04515a977c
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
feat: a wanted list — follow artists on release-type profiles; requests and re-acquisition become wants (M502 #5520)
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>
2026-10-10 00:46:22 -04:00

166 lines
7.9 KiB
SQL

-- 0081_wanted.up.sql — the wanted list (Scribe milestone #502, #5520).
--
-- With Lidarr gone, Minstrel keeps its own record of what it is looking for:
--
-- release_type_profiles which kinds of release a follow wants: primary
-- types (Album, EP, Single…) and the secondary types
-- allowed (Live, Compilation, Remix…). An empty
-- secondary list means studio releases only, which
-- is how live recordings are left out.
-- follows artists whose new releases are wanted, each on a
-- profile.
-- follow_seen_groups release groups a follow has already judged, so
-- each is judged once.
-- wants release groups wanted: a request, a followed
-- artist's new release, or an album whose files went
-- missing. A want is held once the library has a
-- full copy (internal/wanted).
--
-- lidarr_requests becomes requests with no Lidarr columns: approving a request
-- now records a want or a follow instead of sending an add to Lidarr. The
-- missing-file sweeper's attempt bookkeeping goes too; retrying belongs to the
-- search schedule (#5521), which works per want.
CREATE TABLE release_type_profiles (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
name text NOT NULL UNIQUE,
primary_types text[] NOT NULL,
secondary_types text[] NOT NULL DEFAULT '{}',
is_default boolean NOT NULL DEFAULT false,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
CONSTRAINT release_type_profiles_primary_nonempty CHECK (cardinality(primary_types) > 0)
);
-- At most one default; the service keeps exactly one by moving it.
CREATE UNIQUE INDEX release_type_profiles_one_default
ON release_type_profiles (is_default) WHERE is_default;
-- Lidarr's "Standard" metadata profile, which every followed artist used
-- (note #5527): albums, EPs and singles, studio releases only.
INSERT INTO release_type_profiles (name, primary_types, secondary_types, is_default)
VALUES ('Standard', ARRAY['Album', 'EP', 'Single'], '{}', true);
CREATE TABLE follows (
artist_mbid text PRIMARY KEY,
artist_name text NOT NULL,
-- RESTRICT: a profile in use cannot be deleted out from under a follow.
profile_id uuid NOT NULL REFERENCES release_type_profiles (id) ON DELETE RESTRICT,
followed_by uuid REFERENCES users (id) ON DELETE SET NULL,
followed_at timestamptz NOT NULL DEFAULT now(),
-- When the first discography read marked everything then listed as seen.
-- NULL until then: a follow wants only what appears after it.
baseline_at timestamptz,
-- Whether the baseline also wants what the artist had already released
-- that the profile passes and the library lacks: an approved artist
-- request, or the operator ticking "also want…". Cleared once applied.
want_existing boolean NOT NULL DEFAULT false,
-- The discography read this follow last applied.
checked_at timestamptz,
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX follows_profile_idx ON follows (profile_id);
CREATE TABLE follow_seen_groups (
artist_mbid text NOT NULL REFERENCES follows (artist_mbid) ON DELETE CASCADE,
release_group_mbid text NOT NULL,
seen_at timestamptz NOT NULL DEFAULT now(),
PRIMARY KEY (artist_mbid, release_group_mbid)
);
CREATE TABLE wants (
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
release_group_mbid text NOT NULL UNIQUE,
artist_mbid text NOT NULL,
artist_name text NOT NULL,
title text NOT NULL,
primary_type text NOT NULL DEFAULT '',
first_release_date text NOT NULL DEFAULT '',
reason text NOT NULL
CONSTRAINT wants_reason_check CHECK (reason IN ('request', 'follow', 'missing')),
-- The library album this want is about (a missing want), or the one that
-- answered it.
album_id uuid REFERENCES albums (id) ON DELETE SET NULL,
created_by uuid REFERENCES users (id) ON DELETE SET NULL,
-- When it was last wanted: set again when a held want is reopened, so
-- "arrived since it was wanted" measures from the reopening.
wanted_at timestamptz NOT NULL DEFAULT now(),
fulfilled_at timestamptz,
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX wants_open_idx ON wants (wanted_at) WHERE fulfilled_at IS NULL;
CREATE INDEX wants_artist_idx ON wants (artist_mbid);
-- Requests lose Lidarr.
DROP TABLE missing_reacquisitions;
ALTER TABLE lidarr_requests RENAME TO requests;
ALTER TYPE lidarr_request_status RENAME TO request_status;
ALTER TYPE lidarr_request_kind RENAME TO request_kind;
ALTER TABLE requests RENAME COLUMN lidarr_artist_mbid TO artist_mbid;
ALTER TABLE requests RENAME COLUMN lidarr_album_mbid TO album_mbid;
ALTER TABLE requests RENAME COLUMN lidarr_track_mbid TO track_mbid;
ALTER TABLE requests DROP COLUMN quality_profile_id;
ALTER TABLE requests DROP COLUMN root_folder_path;
ALTER TABLE requests DROP COLUMN lidarr_add_confirmed_at;
ALTER INDEX lidarr_requests_user_id_idx RENAME TO requests_user_id_idx;
ALTER INDEX lidarr_requests_status_idx RENAME TO requests_status_idx;
ALTER INDEX lidarr_requests_artist_mbid_idx RENAME TO requests_artist_mbid_idx;
ALTER INDEX lidarr_requests_album_mbid_idx RENAME TO requests_album_mbid_idx;
-- Requests made before #5241 name albums by release id. A want is keyed by
-- release group, so repoint every request the library can translate.
UPDATE requests r
SET album_mbid = a.release_group_mbid,
updated_at = now()
FROM albums a
WHERE a.mbid = r.album_mbid
AND a.release_group_mbid IS NOT NULL
AND a.release_group_mbid <> r.album_mbid;
-- Approved album and track requests still waiting become wants, judged by
-- the same test as any other (internal/wanted).
INSERT INTO wants (release_group_mbid, artist_mbid, artist_name, title, reason, created_by, wanted_at)
SELECT DISTINCT ON (r.album_mbid)
r.album_mbid, r.artist_mbid, r.artist_name, COALESCE(r.album_title, ''), 'request', r.user_id, r.requested_at
FROM requests r
WHERE r.status = 'approved'
AND r.kind IN ('album', 'track')
AND r.album_mbid IS NOT NULL
ORDER BY r.album_mbid, r.requested_at
ON CONFLICT (release_group_mbid) DO NOTHING;
-- The library's album artists start out followed on the default profile
-- (operator, 2026-10-09: "we already have artists in minstrel those start as
-- the wanted artists"). Various Artists and MusicBrainz's bracketed
-- placeholders ([unknown], [anonymous]…) are not artists anyone follows.
INSERT INTO follows (artist_mbid, artist_name, profile_id)
SELECT ar.mbid, ar.name, p.id
FROM artists ar
CROSS JOIN (SELECT id FROM release_type_profiles WHERE is_default) p
WHERE ar.mbid IS NOT NULL AND ar.mbid <> ''
AND ar.mbid <> '89ad4ac3-39f7-470e-963a-56509c546377'
AND ar.name NOT LIKE '[%]'
AND EXISTS (SELECT 1 FROM albums al WHERE al.artist_id = ar.id)
ON CONFLICT (artist_mbid) DO NOTHING;
-- An approved artist request was an artist added to Lidarr: it is followed.
INSERT INTO follows (artist_mbid, artist_name, profile_id, followed_by)
SELECT DISTINCT ON (r.artist_mbid) r.artist_mbid, r.artist_name, p.id, r.decided_by
FROM requests r
CROSS JOIN (SELECT id FROM release_type_profiles WHERE is_default) p
WHERE r.status = 'approved' AND r.kind = 'artist'
ORDER BY r.artist_mbid, r.requested_at
ON CONFLICT (artist_mbid) DO NOTHING;
-- The sweeper now only records a want; how often to look again is the
-- search schedule's business.
ALTER TABLE reacquisition_settings
DROP COLUMN backoff_base_hours,
DROP COLUMN backoff_max_hours,
DROP COLUMN max_attempts,
DROP COLUMN auto_approve;