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
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:
@@ -15,9 +15,6 @@ import (
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarr"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrconfig"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/lidarrquarantine"
|
||||
)
|
||||
|
||||
// newAdminQuarantineRouter builds a test chi router with the five admin
|
||||
@@ -30,7 +27,6 @@ func newAdminQuarantineRouter(h *handlers) chi.Router {
|
||||
admin.Get("/quarantine", h.handleListAdminQuarantine)
|
||||
admin.Post("/quarantine/{track_id}/resolve", h.handleResolveQuarantine)
|
||||
admin.Post("/quarantine/{track_id}/delete-file", h.handleDeleteQuarantineFile)
|
||||
admin.Post("/quarantine/{track_id}/delete-via-lidarr", h.handleDeleteQuarantineViaLidarr)
|
||||
admin.Get("/quarantine/actions", h.handleListQuarantineActions)
|
||||
})
|
||||
return r
|
||||
@@ -56,27 +52,11 @@ func doAdminQuarantineReq(t *testing.T, h *handlers, method, path string, body [
|
||||
return w
|
||||
}
|
||||
|
||||
// installQuarantineClientFn rewires h.lidarrQuarantine to a fresh Service
|
||||
// with a real clientFn pointing at the (current) saved lidarr_config. This
|
||||
// mirrors testHandlersWithClientFn for the requests test file.
|
||||
func installQuarantineClientFn(t *testing.T, h *handlers) {
|
||||
t.Helper()
|
||||
cfg := lidarrconfig.New(h.pool)
|
||||
clientFn := func() *lidarr.Client {
|
||||
c, err := cfg.Get(context.Background())
|
||||
if err != nil || !c.Enabled || c.BaseURL == "" {
|
||||
return nil
|
||||
}
|
||||
return lidarr.NewClient(c.BaseURL, c.APIKey)
|
||||
}
|
||||
h.lidarrQuarantine = lidarrquarantine.NewService(h.pool, cfg, clientFn, h.dataDir)
|
||||
}
|
||||
|
||||
// flagDirect bypasses the HTTP handler to seed a quarantine row via the
|
||||
// Service for setup. Used to construct admin queue scenarios.
|
||||
func flagDirect(t *testing.T, h *handlers, userID, trackID pgtype.UUID, reason string) {
|
||||
t.Helper()
|
||||
if _, err := h.lidarrQuarantine.Flag(context.Background(), userID, trackID, reason, ""); err != nil {
|
||||
if _, err := h.quarantine.Flag(context.Background(), userID, trackID, reason, ""); err != nil {
|
||||
t.Fatalf("flagDirect: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -110,49 +90,13 @@ func seedTrackOnDisk(t *testing.T, h *handlers, unique string) (dbq.Track, strin
|
||||
return tr, path
|
||||
}
|
||||
|
||||
// seedTrackWithAlbumMBID seeds an artist+album-with-mbid+track. The mbid is
|
||||
// required for delete-via-lidarr scenarios.
|
||||
func seedTrackWithAlbumMBID(t *testing.T, h *handlers, unique, albumMBID string) (dbq.Track, string) {
|
||||
t.Helper()
|
||||
artist := seedArtist(t, h.pool, "DvL Artist "+unique)
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "track-"+unique+".mp3")
|
||||
if err := os.WriteFile(path, []byte("audio"), 0o644); err != nil {
|
||||
t.Fatalf("write file: %v", err)
|
||||
}
|
||||
mbid := albumMBID
|
||||
album, err := dbq.New(h.pool).UpsertAlbum(context.Background(), dbq.UpsertAlbumParams{
|
||||
Title: "DvL Album " + unique, SortTitle: "DvL Album " + unique,
|
||||
ArtistID: artist.ID, Mbid: &mbid,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertAlbum: %v", err)
|
||||
}
|
||||
one := int32(1)
|
||||
tr, err := dbq.New(h.pool).UpsertTrack(context.Background(), dbq.UpsertTrackParams{
|
||||
Title: "DvL Track " + unique,
|
||||
AlbumID: album.ID,
|
||||
ArtistID: artist.ID,
|
||||
TrackNumber: &one,
|
||||
DurationMs: 30000,
|
||||
FilePath: path,
|
||||
FileSize: 5,
|
||||
FileFormat: "mp3",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertTrack: %v", err)
|
||||
}
|
||||
return tr, path
|
||||
}
|
||||
|
||||
// TestListAdminQuarantine_AggregatedShape seeds 3 users flagging the same
|
||||
// track with mixed reasons. Verifies one aggregated row with report_count=3,
|
||||
// reason_counts populated, and a Reports list of length 3.
|
||||
func TestListAdminQuarantine_AggregatedShape(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
resetRequests(t, h)
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
bob := seedUser(t, pool, "bob", "pw", false)
|
||||
@@ -192,8 +136,7 @@ func TestListAdminQuarantine_AggregatedShape(t *testing.T) {
|
||||
func TestResolveQuarantine_HappyPath(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
resetRequests(t, h)
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
bob := seedUser(t, pool, "bob", "pw", false)
|
||||
@@ -227,7 +170,7 @@ func TestResolveQuarantine_HappyPath(t *testing.T) {
|
||||
if len(actions) != 1 {
|
||||
t.Fatalf("audit rows = %d, want 1", len(actions))
|
||||
}
|
||||
if actions[0].Action != dbq.LidarrQuarantineActionKindResolved {
|
||||
if actions[0].Action != dbq.QuarantineActionKindResolved {
|
||||
t.Errorf("audit action = %v, want resolved", actions[0].Action)
|
||||
}
|
||||
}
|
||||
@@ -237,8 +180,7 @@ func TestResolveQuarantine_HappyPath(t *testing.T) {
|
||||
func TestDeleteQuarantineFile_HappyPath(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
resetRequests(t, h)
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
admin := seedUser(t, pool, "admin", "pw", true)
|
||||
@@ -268,141 +210,12 @@ func TestDeleteQuarantineFile_HappyPath(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteQuarantineViaLidarr_HappyPath uses a stub Lidarr that returns
|
||||
// a single album for the lookup and 200 for the delete. Verifies the
|
||||
// audit row is written and the local track row is deleted.
|
||||
func TestDeleteQuarantineViaLidarr_HappyPath(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
|
||||
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
if r.URL.Path == "/api/v1/album" && r.Method == http.MethodGet {
|
||||
_, _ = w.Write([]byte(`[{"id":42,"foreignAlbumId":"al-mbid-dvl","title":"Al","artistId":7}]`))
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(stub.Close)
|
||||
saveLidarrConfig(t, h, stub.URL, true)
|
||||
installQuarantineClientFn(t, h) // re-install after config save so clientFn picks it up
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
admin := seedUser(t, pool, "admin", "pw", true)
|
||||
|
||||
track, _ := seedTrackWithAlbumMBID(t, h, "dvl-happy", "al-mbid-dvl")
|
||||
flagDirect(t, h, alice.ID, track.ID, "bad_rip")
|
||||
|
||||
w := doAdminQuarantineReq(t, h, http.MethodPost,
|
||||
"/api/admin/quarantine/"+uuidToString(track.ID)+"/delete-via-lidarr", nil, admin)
|
||||
if w.Code != http.StatusOK {
|
||||
t.Fatalf("status = %d, want 200; body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var got actionResultView
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil {
|
||||
t.Fatalf("decode: %v; body = %s", err, w.Body.String())
|
||||
}
|
||||
if got.AffectedUsers != 1 {
|
||||
t.Errorf("affected_users = %d, want 1", got.AffectedUsers)
|
||||
}
|
||||
if got.DeletedTrackCount == nil || *got.DeletedTrackCount != 1 {
|
||||
t.Errorf("deleted_track_count = %v, want 1", got.DeletedTrackCount)
|
||||
}
|
||||
|
||||
// Track row should be gone.
|
||||
if _, err := dbq.New(pool).GetTrackByID(context.Background(), track.ID); err == nil {
|
||||
t.Error("track row still exists after delete-via-lidarr")
|
||||
}
|
||||
// Audit row written with the lidarr_album_mbid.
|
||||
actions, err := dbq.New(pool).ListQuarantineActions(context.Background(), 50)
|
||||
if err != nil {
|
||||
t.Fatalf("list actions: %v", err)
|
||||
}
|
||||
if len(actions) != 1 {
|
||||
t.Fatalf("audit rows = %d, want 1", len(actions))
|
||||
}
|
||||
if actions[0].Action != dbq.LidarrQuarantineActionKindDeletedViaLidarr {
|
||||
t.Errorf("audit action = %v, want deleted_via_lidarr", actions[0].Action)
|
||||
}
|
||||
if actions[0].LidarrAlbumMbid == nil || *actions[0].LidarrAlbumMbid != "al-mbid-dvl" {
|
||||
t.Errorf("audit lidarr_album_mbid = %v", actions[0].LidarrAlbumMbid)
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteQuarantineViaLidarr_LidarrDisabled verifies 503 when Lidarr is
|
||||
// not configured.
|
||||
func TestDeleteQuarantineViaLidarr_LidarrDisabled(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
admin := seedUser(t, pool, "admin", "pw", true)
|
||||
|
||||
track, _ := seedTrackWithAlbumMBID(t, h, "dvl-disabled", "al-mbid-disabled")
|
||||
flagDirect(t, h, alice.ID, track.ID, "bad_rip")
|
||||
|
||||
// Lidarr config is reset (disabled).
|
||||
w := doAdminQuarantineReq(t, h, http.MethodPost,
|
||||
"/api/admin/quarantine/"+uuidToString(track.ID)+"/delete-via-lidarr", nil, admin)
|
||||
if w.Code != http.StatusServiceUnavailable {
|
||||
t.Fatalf("status = %d, want 503; body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v; body = %s", err, w.Body.String())
|
||||
}
|
||||
if resp["error"] != "lidarr_disabled" {
|
||||
t.Errorf("error = %q, want lidarr_disabled", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeleteQuarantineViaLidarr_AlbumMBIDMissing verifies 404 album_mbid_missing
|
||||
// when the parent album has no mbid.
|
||||
func TestDeleteQuarantineViaLidarr_AlbumMBIDMissing(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
|
||||
stub := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
t.Cleanup(stub.Close)
|
||||
saveLidarrConfig(t, h, stub.URL, true)
|
||||
installQuarantineClientFn(t, h)
|
||||
|
||||
alice := seedUser(t, pool, "alice", "pw", false)
|
||||
admin := seedUser(t, pool, "admin", "pw", true)
|
||||
|
||||
// Track whose album has NO mbid.
|
||||
track, _ := seedTrackOnDisk(t, h, "no-mbid")
|
||||
flagDirect(t, h, alice.ID, track.ID, "bad_rip")
|
||||
|
||||
w := doAdminQuarantineReq(t, h, http.MethodPost,
|
||||
"/api/admin/quarantine/"+uuidToString(track.ID)+"/delete-via-lidarr", nil, admin)
|
||||
if w.Code != http.StatusNotFound {
|
||||
t.Fatalf("status = %d, want 404; body = %s", w.Code, w.Body.String())
|
||||
}
|
||||
var resp map[string]string
|
||||
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v; body = %s", err, w.Body.String())
|
||||
}
|
||||
if resp["error"] != "album_mbid_missing" {
|
||||
t.Errorf("error = %q, want album_mbid_missing", resp["error"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestQuarantineAdminEndpointsRequire403ForNonAdmin verifies all five admin
|
||||
// endpoints reject non-admin callers with 403.
|
||||
func TestQuarantineAdminEndpointsRequire403ForNonAdmin(t *testing.T) {
|
||||
h, pool := testHandlers(t)
|
||||
truncateLibrary(t, pool)
|
||||
resetLidarrState(t, h)
|
||||
installQuarantineClientFn(t, h)
|
||||
resetRequests(t, h)
|
||||
|
||||
nonAdmin := seedUser(t, pool, "regular", "pw", false)
|
||||
fakeID := "00000000-0000-0000-0000-000000000001"
|
||||
@@ -414,7 +227,6 @@ func TestQuarantineAdminEndpointsRequire403ForNonAdmin(t *testing.T) {
|
||||
{http.MethodGet, "/api/admin/quarantine"},
|
||||
{http.MethodPost, "/api/admin/quarantine/" + fakeID + "/resolve"},
|
||||
{http.MethodPost, "/api/admin/quarantine/" + fakeID + "/delete-file"},
|
||||
{http.MethodPost, "/api/admin/quarantine/" + fakeID + "/delete-via-lidarr"},
|
||||
{http.MethodGet, "/api/admin/quarantine/actions"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
|
||||
Reference in New Issue
Block a user