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
+357
View File
@@ -0,0 +1,357 @@
// Code generated by sqlc. DO NOT EDIT.
// versions:
// sqlc v1.31.1
// source: quarantine.sql
package dbq
import (
"context"
"github.com/jackc/pgx/v5/pgtype"
)
const countQuarantineForTrack = `-- name: CountQuarantineForTrack :one
SELECT count(*)::int FROM quarantine WHERE track_id = $1
`
// Reads affected_users for the audit row before the delete fires.
func (q *Queries) CountQuarantineForTrack(ctx context.Context, trackID pgtype.UUID) (int32, error) {
row := q.db.QueryRow(ctx, countQuarantineForTrack, trackID)
var column_1 int32
err := row.Scan(&column_1)
return column_1, err
}
const deleteQuarantine = `-- name: DeleteQuarantine :one
DELETE FROM quarantine
WHERE user_id = $1 AND track_id = $2
RETURNING user_id, track_id, reason, notes, created_at
`
type DeleteQuarantineParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
}
// Removes the caller's row. Returns the deleted row so the handler can
// distinguish "no row existed" (zero rows -> ErrNoRows) from success.
func (q *Queries) DeleteQuarantine(ctx context.Context, arg DeleteQuarantineParams) (Quarantine, error) {
row := q.db.QueryRow(ctx, deleteQuarantine, arg.UserID, arg.TrackID)
var i Quarantine
err := row.Scan(
&i.UserID,
&i.TrackID,
&i.Reason,
&i.Notes,
&i.CreatedAt,
)
return i, err
}
const deleteQuarantineForTrack = `-- name: DeleteQuarantineForTrack :exec
DELETE FROM quarantine WHERE track_id = $1
`
// 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).
func (q *Queries) DeleteQuarantineForTrack(ctx context.Context, trackID pgtype.UUID) error {
_, err := q.db.Exec(ctx, deleteQuarantineForTrack, trackID)
return err
}
const listAdminQuarantineQueue = `-- name: ListAdminQuarantineQueue :many
SELECT
t.id AS track_id,
t.title AS track_title,
ar.name AS artist_name,
al.title AS album_title,
al.id AS album_id,
count(q.user_id)::int AS report_count,
max(q.created_at)::timestamptz AS latest_at
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
ORDER BY max(q.created_at) DESC
`
type ListAdminQuarantineQueueRow struct {
TrackID pgtype.UUID
TrackTitle string
ArtistName string
AlbumTitle string
AlbumID pgtype.UUID
ReportCount int32
LatestAt pgtype.Timestamptz
}
// Aggregated admin queue. One row per track. The handler post-processes
// the rows it gets from this query plus a per-track ListQuarantineReports
// call to materialize reason_counts and the per-user reports list.
func (q *Queries) ListAdminQuarantineQueue(ctx context.Context) ([]ListAdminQuarantineQueueRow, error) {
rows, err := q.db.Query(ctx, listAdminQuarantineQueue)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListAdminQuarantineQueueRow
for rows.Next() {
var i ListAdminQuarantineQueueRow
if err := rows.Scan(
&i.TrackID,
&i.TrackTitle,
&i.ArtistName,
&i.AlbumTitle,
&i.AlbumID,
&i.ReportCount,
&i.LatestAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listQuarantineActions = `-- name: ListQuarantineActions :many
SELECT id, track_id, track_title, artist_name, album_title, action, admin_id, album_mbid, affected_users, created_at FROM quarantine_actions
ORDER BY created_at DESC
LIMIT $1
`
func (q *Queries) ListQuarantineActions(ctx context.Context, limit int32) ([]QuarantineAction, error) {
rows, err := q.db.Query(ctx, listQuarantineActions, limit)
if err != nil {
return nil, err
}
defer rows.Close()
var items []QuarantineAction
for rows.Next() {
var i QuarantineAction
if err := rows.Scan(
&i.ID,
&i.TrackID,
&i.TrackTitle,
&i.ArtistName,
&i.AlbumTitle,
&i.Action,
&i.AdminID,
&i.AlbumMbid,
&i.AffectedUsers,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listQuarantineForUser = `-- name: ListQuarantineForUser :many
SELECT
q.user_id, q.track_id, q.reason, q.notes, q.created_at,
t.id AS track_id_join,
t.title AS track_title,
t.duration_ms AS track_duration_ms,
al.id AS album_id,
al.title AS album_title,
al.cover_art_path AS album_cover_art_path,
ar.id AS artist_id,
ar.name AS artist_name
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
WHERE q.user_id = $1
ORDER BY q.created_at DESC
`
type ListQuarantineForUserRow struct {
Quarantine Quarantine
TrackIDJoin pgtype.UUID
TrackTitle string
TrackDurationMs int32
AlbumID pgtype.UUID
AlbumTitle string
AlbumCoverArtPath *string
ArtistID pgtype.UUID
ArtistName string
}
// Caller's own quarantines plus just the joined fields the SPA's
// /library/hidden actually renders. Embedding the full tracks/albums/artists
// structs would drag ~28 columns of file_path / sort_* / mbid / etc. that
// the row card never uses.
func (q *Queries) ListQuarantineForUser(ctx context.Context, userID pgtype.UUID) ([]ListQuarantineForUserRow, error) {
rows, err := q.db.Query(ctx, listQuarantineForUser, userID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListQuarantineForUserRow
for rows.Next() {
var i ListQuarantineForUserRow
if err := rows.Scan(
&i.Quarantine.UserID,
&i.Quarantine.TrackID,
&i.Quarantine.Reason,
&i.Quarantine.Notes,
&i.Quarantine.CreatedAt,
&i.TrackIDJoin,
&i.TrackTitle,
&i.TrackDurationMs,
&i.AlbumID,
&i.AlbumTitle,
&i.AlbumCoverArtPath,
&i.ArtistID,
&i.ArtistName,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const listQuarantineReportsForTrack = `-- name: ListQuarantineReportsForTrack :many
SELECT
q.user_id,
u.username,
q.reason,
q.notes,
q.created_at
FROM quarantine q
JOIN users u ON u.id = q.user_id
WHERE q.track_id = $1
ORDER BY q.created_at DESC
`
type ListQuarantineReportsForTrackRow struct {
UserID pgtype.UUID
Username string
Reason QuarantineReason
Notes *string
CreatedAt pgtype.Timestamptz
}
// Per-user reports for a single track. Returned by ListAdminQuarantineQueue
// post-processing and exposed expandable in the SPA admin queue rows.
func (q *Queries) ListQuarantineReportsForTrack(ctx context.Context, trackID pgtype.UUID) ([]ListQuarantineReportsForTrackRow, error) {
rows, err := q.db.Query(ctx, listQuarantineReportsForTrack, trackID)
if err != nil {
return nil, err
}
defer rows.Close()
var items []ListQuarantineReportsForTrackRow
for rows.Next() {
var i ListQuarantineReportsForTrackRow
if err := rows.Scan(
&i.UserID,
&i.Username,
&i.Reason,
&i.Notes,
&i.CreatedAt,
); err != nil {
return nil, err
}
items = append(items, i)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
const upsertQuarantine = `-- name: UpsertQuarantine :one
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,
notes = EXCLUDED.notes,
created_at = now()
RETURNING user_id, track_id, reason, notes, created_at
`
type UpsertQuarantineParams struct {
UserID pgtype.UUID
TrackID pgtype.UUID
Reason QuarantineReason
Notes *string
}
// Insert a new quarantine row, or update reason/notes if the user has
// already flagged this track.
func (q *Queries) UpsertQuarantine(ctx context.Context, arg UpsertQuarantineParams) (Quarantine, error) {
row := q.db.QueryRow(ctx, upsertQuarantine,
arg.UserID,
arg.TrackID,
arg.Reason,
arg.Notes,
)
var i Quarantine
err := row.Scan(
&i.UserID,
&i.TrackID,
&i.Reason,
&i.Notes,
&i.CreatedAt,
)
return i, err
}
const writeQuarantineAction = `-- name: WriteQuarantineAction :one
INSERT INTO quarantine_actions (
track_id, track_title, artist_name, album_title,
action, admin_id, affected_users
) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, track_id, track_title, artist_name, album_title, action, admin_id, album_mbid, affected_users, created_at
`
type WriteQuarantineActionParams struct {
TrackID pgtype.UUID
TrackTitle string
ArtistName string
AlbumTitle *string
Action QuarantineActionKind
AdminID pgtype.UUID
AffectedUsers int32
}
// Audit row for an admin destructive action.
func (q *Queries) WriteQuarantineAction(ctx context.Context, arg WriteQuarantineActionParams) (QuarantineAction, error) {
row := q.db.QueryRow(ctx, writeQuarantineAction,
arg.TrackID,
arg.TrackTitle,
arg.ArtistName,
arg.AlbumTitle,
arg.Action,
arg.AdminID,
arg.AffectedUsers,
)
var i QuarantineAction
err := row.Scan(
&i.ID,
&i.TrackID,
&i.TrackTitle,
&i.ArtistName,
&i.AlbumTitle,
&i.Action,
&i.AdminID,
&i.AlbumMbid,
&i.AffectedUsers,
&i.CreatedAt,
)
return i, err
}