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:
@@ -10,22 +10,20 @@ 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/lidarrquarantine"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/quarantine"
|
||||
)
|
||||
|
||||
// adminQueueRowView is a single aggregated row in the admin queue response.
|
||||
type adminQueueRowView struct {
|
||||
TrackID string `json:"track_id"`
|
||||
TrackTitle string `json:"track_title"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumTitle string `json:"album_title"`
|
||||
AlbumID string `json:"album_id"`
|
||||
LidarrAlbumMBID *string `json:"lidarr_album_mbid,omitempty"`
|
||||
ReportCount int32 `json:"report_count"`
|
||||
LatestAt string `json:"latest_at"`
|
||||
ReasonCounts map[string]int `json:"reason_counts"`
|
||||
Reports []adminQueueReportView `json:"reports"`
|
||||
TrackID string `json:"track_id"`
|
||||
TrackTitle string `json:"track_title"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumTitle string `json:"album_title"`
|
||||
AlbumID string `json:"album_id"`
|
||||
ReportCount int32 `json:"report_count"`
|
||||
LatestAt string `json:"latest_at"`
|
||||
ReasonCounts map[string]int `json:"reason_counts"`
|
||||
Reports []adminQueueReportView `json:"reports"`
|
||||
}
|
||||
|
||||
// adminQueueReportView is one user's report under an aggregated admin row.
|
||||
@@ -47,7 +45,7 @@ func formatTimestamp(ts pgtype.Timestamptz) string {
|
||||
|
||||
// handleListAdminQuarantine implements GET /api/admin/quarantine.
|
||||
func (h *handlers) handleListAdminQuarantine(w http.ResponseWriter, r *http.Request) {
|
||||
rows, err := h.lidarrQuarantine.ListAdminQueue(r.Context())
|
||||
rows, err := h.quarantine.ListAdminQueue(r.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("admin: list quarantine", "err", err)
|
||||
writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
|
||||
@@ -66,27 +64,25 @@ func (h *handlers) handleListAdminQuarantine(w http.ResponseWriter, r *http.Requ
|
||||
})
|
||||
}
|
||||
out = append(out, adminQueueRowView{
|
||||
TrackID: uuidToString(row.TrackID),
|
||||
TrackTitle: row.TrackTitle,
|
||||
ArtistName: row.ArtistName,
|
||||
AlbumTitle: row.AlbumTitle,
|
||||
AlbumID: uuidToString(row.AlbumID),
|
||||
LidarrAlbumMBID: row.LidarrAlbumMBID,
|
||||
ReportCount: row.ReportCount,
|
||||
LatestAt: formatTimestamp(row.LatestAt),
|
||||
ReasonCounts: row.ReasonCounts,
|
||||
Reports: reports,
|
||||
TrackID: uuidToString(row.TrackID),
|
||||
TrackTitle: row.TrackTitle,
|
||||
ArtistName: row.ArtistName,
|
||||
AlbumTitle: row.AlbumTitle,
|
||||
AlbumID: uuidToString(row.AlbumID),
|
||||
ReportCount: row.ReportCount,
|
||||
LatestAt: formatTimestamp(row.LatestAt),
|
||||
ReasonCounts: row.ReasonCounts,
|
||||
Reports: reports,
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
}
|
||||
|
||||
// actionResultView is the response shape for the three admin destructive
|
||||
// actions (resolve, delete-file, delete-via-lidarr).
|
||||
// actionResultView is the response shape for the admin actions (resolve,
|
||||
// delete-file).
|
||||
type actionResultView struct {
|
||||
ActionID string `json:"action_id"`
|
||||
AffectedUsers int32 `json:"affected_users"`
|
||||
DeletedTrackCount *int `json:"deleted_track_count,omitempty"`
|
||||
ActionID string `json:"action_id"`
|
||||
AffectedUsers int32 `json:"affected_users"`
|
||||
}
|
||||
|
||||
// handleResolveQuarantine implements POST /api/admin/quarantine/{track_id}/resolve.
|
||||
@@ -101,9 +97,9 @@ func (h *handlers) handleResolveQuarantine(w http.ResponseWriter, r *http.Reques
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
action, err := h.lidarrQuarantine.Resolve(r.Context(), id, admin.ID)
|
||||
action, err := h.quarantine.Resolve(r.Context(), id, admin.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, lidarrquarantine.ErrTrackNotFound) {
|
||||
if errors.Is(err, quarantine.ErrTrackNotFound) {
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "track_not_found")
|
||||
return
|
||||
}
|
||||
@@ -131,7 +127,7 @@ func (h *handlers) handleDeleteQuarantineFile(w http.ResponseWriter, r *http.Req
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
action, err := h.lidarrQuarantine.DeleteFile(r.Context(), id, admin.ID)
|
||||
action, err := h.quarantine.DeleteFile(r.Context(), id, admin.ID)
|
||||
if err != nil {
|
||||
// Written in the enveloped shape, not writeAdminJSONErr's bare code: the
|
||||
// message is the part that tells the operator which directory and uid.
|
||||
@@ -141,7 +137,7 @@ func (h *handlers) handleDeleteQuarantineFile(w http.ResponseWriter, r *http.Req
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, lidarrquarantine.ErrTrackNotFound):
|
||||
case errors.Is(err, quarantine.ErrTrackNotFound):
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "track_not_found")
|
||||
default:
|
||||
h.logger.Error("admin: delete file", "err", err)
|
||||
@@ -158,60 +154,19 @@ func (h *handlers) handleDeleteQuarantineFile(w http.ResponseWriter, r *http.Req
|
||||
})
|
||||
}
|
||||
|
||||
// handleDeleteQuarantineViaLidarr implements POST /api/admin/quarantine/{track_id}/delete-via-lidarr.
|
||||
func (h *handlers) handleDeleteQuarantineViaLidarr(w http.ResponseWriter, r *http.Request) {
|
||||
admin, ok := auth.UserFromContext(r.Context())
|
||||
if !ok {
|
||||
writeAdminJSONErr(w, http.StatusUnauthorized, "unauthorized")
|
||||
return
|
||||
}
|
||||
id, ok := parseUUID(chi.URLParam(r, "track_id"))
|
||||
if !ok {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
action, deleted, err := h.lidarrQuarantine.DeleteViaLidarr(r.Context(), id, admin.ID)
|
||||
if err != nil {
|
||||
switch {
|
||||
case errors.Is(err, lidarrquarantine.ErrLidarrDisabled):
|
||||
writeAdminJSONErr(w, http.StatusServiceUnavailable, "lidarr_disabled")
|
||||
case errors.Is(err, lidarrquarantine.ErrTrackNotFound):
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "track_not_found")
|
||||
case errors.Is(err, lidarrquarantine.ErrAlbumMBIDMissing):
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "album_mbid_missing")
|
||||
case errors.Is(err, lidarrquarantine.ErrLidarrAlbumNotFound):
|
||||
writeAdminJSONErr(w, http.StatusBadGateway, "lidarr_album_lookup_failed")
|
||||
case errors.Is(err, lidarr.ErrUnreachable):
|
||||
writeAdminJSONErr(w, http.StatusServiceUnavailable, "lidarr_unreachable")
|
||||
case errors.Is(err, lidarr.ErrAuthFailed):
|
||||
writeAdminJSONErr(w, http.StatusServiceUnavailable, "lidarr_auth_failed")
|
||||
default:
|
||||
h.logger.Error("admin: delete via lidarr", "err", err)
|
||||
writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
// Broadcast: same rationale as delete-file — track row gone everywhere.
|
||||
h.publishQuarantineEvent("quarantine.deleted_via_lidarr", pgtype.UUID{}, id, true)
|
||||
writeJSON(w, http.StatusOK, actionResultView{
|
||||
ActionID: uuidToString(action.ID),
|
||||
AffectedUsers: action.AffectedUsers,
|
||||
DeletedTrackCount: &deleted,
|
||||
})
|
||||
}
|
||||
|
||||
// actionLogView is the row shape returned by GET /api/admin/quarantine/actions.
|
||||
type actionLogView struct {
|
||||
ID string `json:"id"`
|
||||
TrackID string `json:"track_id"`
|
||||
TrackTitle string `json:"track_title"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumTitle *string `json:"album_title,omitempty"`
|
||||
Action string `json:"action"`
|
||||
AdminID *string `json:"admin_id,omitempty"`
|
||||
LidarrAlbumMBID *string `json:"lidarr_album_mbid,omitempty"`
|
||||
AffectedUsers int32 `json:"affected_users"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
ID string `json:"id"`
|
||||
TrackID string `json:"track_id"`
|
||||
TrackTitle string `json:"track_title"`
|
||||
ArtistName string `json:"artist_name"`
|
||||
AlbumTitle *string `json:"album_title,omitempty"`
|
||||
Action string `json:"action"`
|
||||
AdminID *string `json:"admin_id,omitempty"`
|
||||
// AlbumMBID is set on the older deleted_album rows: the album removed.
|
||||
AlbumMBID *string `json:"album_mbid,omitempty"`
|
||||
AffectedUsers int32 `json:"affected_users"`
|
||||
CreatedAt string `json:"created_at"`
|
||||
}
|
||||
|
||||
// handleListQuarantineActions implements GET /api/admin/quarantine/actions.
|
||||
@@ -241,16 +196,16 @@ func (h *handlers) handleListQuarantineActions(w http.ResponseWriter, r *http.Re
|
||||
adminID = &s
|
||||
}
|
||||
out = append(out, actionLogView{
|
||||
ID: uuidToString(row.ID),
|
||||
TrackID: uuidToString(row.TrackID),
|
||||
TrackTitle: row.TrackTitle,
|
||||
ArtistName: row.ArtistName,
|
||||
AlbumTitle: row.AlbumTitle,
|
||||
Action: string(row.Action),
|
||||
AdminID: adminID,
|
||||
LidarrAlbumMBID: row.LidarrAlbumMbid,
|
||||
AffectedUsers: row.AffectedUsers,
|
||||
CreatedAt: formatTimestamp(row.CreatedAt),
|
||||
ID: uuidToString(row.ID),
|
||||
TrackID: uuidToString(row.TrackID),
|
||||
TrackTitle: row.TrackTitle,
|
||||
ArtistName: row.ArtistName,
|
||||
AlbumTitle: row.AlbumTitle,
|
||||
Action: string(row.Action),
|
||||
AdminID: adminID,
|
||||
AlbumMBID: row.AlbumMbid,
|
||||
AffectedUsers: row.AffectedUsers,
|
||||
CreatedAt: formatTimestamp(row.CreatedAt),
|
||||
})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, out)
|
||||
|
||||
Reference in New Issue
Block a user