feat: Minstrel no longer talks to Lidarr: its connection, Delete via Lidarr and the unmonitor options are gone, and the track reports are plain quarantine (M502 #5525)
release / govulncheck (push) Successful in 15s
release / web (push) Failing after 17s
release / go (push) Successful in 1m13s
release / integration (push) Successful in 4m56s
release / android (push) Successful in 5m9s
release / Build signed APK (releases and dev) (push) Successful in 5m19s
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

Migration 0091 drops lidarr_config and renames the quarantine tables, types,
indexes and the history column off the Lidarr name. The Integrations Lidarr
card, the Reports "Delete via Lidarr" action, the merge and remove-track
unmonitor options, the Lidarr audit actions and error codes go, on the
server, web and Android. The README describes acquisition as it is now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-11 13:48:20 -04:00
co-authored by Claude Opus 5.5
parent c54792c51e
commit 3a16732883
170 changed files with 994 additions and 6242 deletions
+2 -2
View File
@@ -11,11 +11,11 @@ SELECT
-- it, or it failed to play (#5606). Counted by track, as the Reports
-- page lists them.
(SELECT count(*) FROM (
SELECT track_id FROM lidarr_quarantine
SELECT track_id FROM quarantine
UNION
SELECT track_id FROM playback_errors WHERE resolved_at IS NULL) r)::bigint AS reports,
(SELECT min(at) FROM (
SELECT created_at AS at FROM lidarr_quarantine
SELECT created_at AS at FROM quarantine
UNION ALL
SELECT occurred_at FROM playback_errors WHERE resolved_at IS NULL) r)::timestamptz AS reports_since,
(SELECT count(*) FROM duplicate_groups g
+2 -2
View File
@@ -15,8 +15,8 @@ SELECT count(*)::bigint FROM audit_log
-- name: ListAuditLogByActions :many
-- One page of the given actions, newest first: the duplicates report's
-- "resolved automatically" activity (M498) reads the resolver's merges and
-- Lidarr release changes from here.
-- "resolved automatically" activity (M498) reads the resolver's merges from
-- here.
SELECT * FROM audit_log
WHERE action = ANY(sqlc.arg(actions)::text[])
AND (NOT sqlc.arg(system_only)::boolean OR actor_id IS NULL)
+4 -4
View File
@@ -41,7 +41,7 @@ SELECT t.id, t.album_id, t.artist_id
WHERE gl.user_id = $1 AND gl.track_id = t.id
)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY md5(t.id::text || $2::text)
@@ -74,7 +74,7 @@ SELECT t.id, t.album_id, t.artist_id
WHERE ml.user_id = $1 AND ml.track_id = t.id
)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
GROUP BY t.id, t.album_id, t.artist_id
@@ -100,7 +100,7 @@ SELECT t.id, t.album_id, t.artist_id
WHERE gl.user_id = $1 AND gl.track_id = t.id
)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY md5(t.id::text || $2::text)
@@ -144,7 +144,7 @@ WITH scored AS (
WHERE gl.user_id = sqlc.arg(user_id) AND gl.track_id = t.id
)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id
)
GROUP BY t.id, t.album_id, t.artist_id
+1 -1
View File
@@ -16,7 +16,7 @@ SELECT pe.id AS event_id,
WHERE pe.user_id = $1
AND pe.was_skipped = false
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY pe.started_at DESC
-20
View File
@@ -1,20 +0,0 @@
-- name: GetLidarrConfig :one
SELECT id, enabled, base_url, api_key, default_quality_profile_id,
default_metadata_profile_id, default_root_folder_path,
created_at, updated_at
FROM lidarr_config
WHERE id = 1;
-- name: UpdateLidarrConfig :one
UPDATE lidarr_config
SET enabled = $1,
base_url = $2,
api_key = $3,
default_quality_profile_id = $4,
default_metadata_profile_id = $5,
default_root_folder_path = $6,
updated_at = now()
WHERE id = 1
RETURNING id, enabled, base_url, api_key, default_quality_profile_id,
default_metadata_profile_id, default_root_folder_path,
created_at, updated_at;
+2 -4
View File
@@ -13,11 +13,9 @@ SELECT id, tier, status
FOR UPDATE;
-- name: ListDuplicateGroupMergeMembers :many
SELECT t.id, t.file_path, t.file_format, t.file_size, t.added_at, t.album_id,
t.mbid, albums.mbid AS album_mbid
SELECT t.id, t.file_path, t.file_format, t.file_size, t.added_at, t.album_id
FROM duplicate_group_members m
JOIN tracks t ON t.id = m.track_id
JOIN albums ON albums.id = t.album_id
JOIN tracks t ON t.id = m.track_id
WHERE m.group_id = sqlc.arg(group_id)
ORDER BY t.id;
+2 -2
View File
@@ -1,6 +1,6 @@
-- Missing-pair pass (M485). A track marked missing whose replacement is already
-- on disk under another row — most often a Lidarr quality upgrade, mp3 → flac at
-- a new path — is folded into that replacement, so its history moves across and
-- on disk under another row — most often a quality upgrade, mp3 → flac at a new
-- path — is folded into that replacement, so its history moves across and
-- re-acquisition stops chasing music the library already has.
--
-- Move adoption (#2528) cannot catch these: it links a new file to a missing row
@@ -1,7 +1,7 @@
-- name: UpsertQuarantine :one
-- Insert a new quarantine row, or update reason/notes if the user has
-- already flagged this track.
INSERT INTO lidarr_quarantine (user_id, track_id, reason, notes)
INSERT INTO quarantine (user_id, track_id, reason, notes)
VALUES ($1, $2, $3, $4)
ON CONFLICT (user_id, track_id) DO UPDATE SET
reason = EXCLUDED.reason,
@@ -12,7 +12,7 @@ RETURNING user_id, track_id, reason, notes, created_at;
-- name: DeleteQuarantine :one
-- Removes the caller's row. Returns the deleted row so the handler can
-- distinguish "no row existed" (zero rows -> ErrNoRows) from success.
DELETE FROM lidarr_quarantine
DELETE FROM quarantine
WHERE user_id = $1 AND track_id = $2
RETURNING user_id, track_id, reason, notes, created_at;
@@ -31,7 +31,7 @@ SELECT
al.cover_art_path AS album_cover_art_path,
ar.id AS artist_id,
ar.name AS artist_name
FROM lidarr_quarantine q
FROM quarantine q
JOIN tracks t ON t.id = q.track_id
JOIN albums al ON al.id = t.album_id
JOIN artists ar ON ar.id = t.artist_id
@@ -48,14 +48,13 @@ SELECT
ar.name AS artist_name,
al.title AS album_title,
al.id AS album_id,
al.mbid AS lidarr_album_mbid,
count(q.user_id)::int AS report_count,
max(q.created_at)::timestamptz AS latest_at
FROM lidarr_quarantine q
FROM quarantine q
JOIN tracks t ON t.id = q.track_id
JOIN albums al ON al.id = t.album_id
JOIN artists ar ON ar.id = t.artist_id
GROUP BY t.id, ar.name, al.title, al.id, al.mbid
GROUP BY t.id, ar.name, al.title, al.id
ORDER BY max(q.created_at) DESC;
-- name: ListQuarantineReportsForTrack :many
@@ -67,7 +66,7 @@ SELECT
q.reason,
q.notes,
q.created_at
FROM lidarr_quarantine q
FROM quarantine q
JOIN users u ON u.id = q.user_id
WHERE q.track_id = $1
ORDER BY q.created_at DESC;
@@ -76,21 +75,21 @@ ORDER BY q.created_at DESC;
-- Clears all per-user rows for a given track. Used by Resolve and the
-- two delete actions. Caller writes the audit row separately before
-- this fires (so we can capture the affected_users count).
DELETE FROM lidarr_quarantine WHERE track_id = $1;
DELETE FROM quarantine WHERE track_id = $1;
-- name: CountQuarantineForTrack :one
-- Reads affected_users for the audit row before the delete fires.
SELECT count(*)::int FROM lidarr_quarantine WHERE track_id = $1;
SELECT count(*)::int FROM quarantine WHERE track_id = $1;
-- name: WriteQuarantineAction :one
-- Audit row for an admin destructive action.
INSERT INTO lidarr_quarantine_actions (
INSERT INTO quarantine_actions (
track_id, track_title, artist_name, album_title,
action, admin_id, lidarr_album_mbid, affected_users
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
action, admin_id, affected_users
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING *;
-- name: ListQuarantineActions :many
SELECT * FROM lidarr_quarantine_actions
SELECT * FROM quarantine_actions
ORDER BY created_at DESC
LIMIT $1;
+4 -4
View File
@@ -31,7 +31,7 @@ WHERE t.id <> $2
AND started_at > now() - $3 * interval '1 hour'
)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
);
@@ -83,7 +83,7 @@ excluded_ids AS (
WHERE pe.user_id = $1 AND pe.started_at > now() - $3 * interval '1 hour'
UNION ALL
SELECT q.track_id AS id
FROM lidarr_quarantine q
FROM quarantine q
WHERE q.user_id = $1
),
lb_similar AS (
@@ -416,7 +416,7 @@ JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY p.cnt DESC, t.id
@@ -443,7 +443,7 @@ JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = sqlc.arg(artist_id)
AND t.missing_since IS NULL -- #2523: never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = sqlc.arg(user_id) AND q.track_id = t.id
)
ORDER BY p.cnt DESC, t.id
+5 -5
View File
@@ -44,7 +44,7 @@ SELECT t.id, t.album_id, t.artist_id
AND COALESCE(pc.c, 0) <= 2
AND COALESCE(sc.c, 0) < 2
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY md5(t.id::text || $2::text)
@@ -75,7 +75,7 @@ SELECT t.id, t.album_id, t.artist_id
AND s.c >= 3
AND s.last_at <= now() - interval '30 days'
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
-- Declined-resurfacing cooldown (#1258): a skip on a
@@ -153,7 +153,7 @@ SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
JOIN albums_tiered alt ON alt.album_id = t.album_id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY alt.tier, alt.created_at DESC, t.album_id,
@@ -192,7 +192,7 @@ SELECT t.id, t.album_id, t.artist_id
JOIN windowed w ON w.track_id = t.id
WHERE t.missing_since IS NULL -- #2523: never offer a file that is gone
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY w.c DESC, md5(t.id::text || $2::text)
@@ -248,7 +248,7 @@ SELECT t.id, t.album_id, t.artist_id, alt.tier::int AS tier
AND alt.tier IS NOT NULL
AND NOT EXISTS (SELECT 1 FROM attempted a WHERE a.track_id = t.id)
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY
+1 -1
View File
@@ -24,7 +24,7 @@ JOIN albums a ON a.id = t.album_id
WHERE pe.user_id = $1
AND pe.started_at > now() - ($2::float8 * INTERVAL '1 day')
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
);
+10 -10
View File
@@ -62,7 +62,7 @@ SELECT * FROM tracks WHERE file_path = $1;
SELECT * FROM tracks
WHERE album_id = $1
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
)
ORDER BY disc_number NULLS LAST, track_number NULLS LAST;
@@ -76,7 +76,7 @@ SELECT count(*) FROM tracks WHERE album_id = $1;
SELECT * FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
)
ORDER BY title
@@ -87,13 +87,13 @@ LIMIT $3 OFFSET $4;
SELECT COUNT(*) FROM tracks
WHERE title ILIKE '%' || $1::text || '%'
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $2 AND q.track_id = tracks.id
);
-- name: ListArtistTracksForUser :many
-- M6a: every track for the artist across their albums, with album_title
-- and artist_name joined. Honors per-user lidarr_quarantine. Used by
-- and artist_name joined. Honors per-user quarantine. Used by
-- /api/artists/{id}/tracks for the artist-card play affordance, which
-- shuffles client-side. Ordering matches album/track natural order so
-- the shuffle has a deterministic input.
@@ -105,7 +105,7 @@ JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE t.artist_id = $1
AND NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $2 AND q.track_id = t.id
)
ORDER BY albums.release_date NULLS LAST, albums.sort_title,
@@ -122,7 +122,7 @@ FROM tracks t
JOIN albums ON albums.id = t.album_id
JOIN artists ON artists.id = t.artist_id
WHERE NOT EXISTS (
SELECT 1 FROM lidarr_quarantine q
SELECT 1 FROM quarantine q
WHERE q.user_id = $1 AND q.track_id = t.id
)
ORDER BY random()
@@ -136,8 +136,8 @@ SELECT COUNT(*) FROM tracks WHERE artist_id = $1;
-- name: DeleteTrack :one
-- M7 #372: hard delete with FK cascade. The CASCADE on track_id from
-- play_events / general_likes_tracks / lidarr_quarantine /
-- lidarr_quarantine_actions handles their cleanup. RETURNING gives us
-- play_events / general_likes_tracks / quarantine /
-- quarantine_actions handles their cleanup. RETURNING gives us
-- album_id + artist_id for the album-empty / artist-empty cascade
-- checks the service does next.
DELETE FROM tracks WHERE id = $1
@@ -321,8 +321,8 @@ UPDATE tracks
AND regexp_replace(file_path, '^.*/', '') ~* sqlc.arg(pattern)::text;
-- name: ClearStaleSuspectSourceTracks :execrows
-- A held-back track whose name no longer carries a marker (renamed, retagged
-- by Lidarr) is released again.
-- A held-back track whose name no longer carries a marker (renamed or
-- retagged) is released again.
UPDATE tracks
SET source_verdict = NULL, source_verdict_at = NULL
WHERE source_verdict = 'suspect'
+1 -1
View File
@@ -96,7 +96,7 @@ RETURNING *;
-- Hard delete with cascade. The schema's ON DELETE CASCADE foreign keys
-- handle plays/likes/sessions/etc. Caller must enforce the last-admin
-- guard at the HTTP layer (count admins, refuse if target is the only
-- admin). The lidarr_quarantine, general_likes*, play_events,
-- admin). The quarantine, general_likes*, play_events,
-- sessions, and other user-FK tables all have ON DELETE CASCADE so
-- this is a clean drop.
DELETE FROM users WHERE id = $1;