fix: a group of one recording in several slots is settled by getting the album again, not merged (#6184)
release / govulncheck (push) Successful in 36s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m45s
release / android (push) Successful in 5m9s
release / integration (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m21s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m23s
release / Verify release artifacts (tag releases only) (push) Skipped
release / govulncheck (push) Successful in 36s
release / web (push) Successful in 1m39s
release / go (push) Successful in 1m45s
release / android (push) Successful in 5m9s
release / integration (push) Successful in 5m11s
release / Build signed APK (releases and dev) (push) Successful in 5m21s
release / Attach APK to the Release (tag releases only) (push) Skipped
release / Build + push container image (push) Successful in 1m23s
release / Verify release artifacts (tag releases only) (push) Skipped
The duplicates review offered only Merge for a wrong-slot group. A merge keeps
one copy's title whether or not it is the audio's, and neither sets the release
aside nor wants the missing song back.
For class mismatch the page now asks which song the recording is. "Get it
again" (the operator can't tell) removes every copy; "Keep <title>" keeps that
copy and removes the ones under other titles. Either way it runs the resolver's
own path: the release the files came from is set aside, the removed copies'
tracks stay marked missing and emptied, and the album is wanted again.
- library.SettleWrongSlot, POST /api/admin/library/duplicates/{id}/refetch
- ListDuplicateGroupsForResolve takes an optional group_id
- removeWrongSlots takes the acting operator (manual: not automatic, actor in
the audit row) and removes files through removeTrackFileOnDisk, so a
read-only library answers library_not_writable
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -449,3 +449,90 @@ func (h *handlers) handleMergeDuplicateGroup(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
// refetchDuplicateRequest is the operator's answer for one recording filling
|
||||
// several of an album's slots. keep_track_id is the copy whose title the
|
||||
// recording is; empty means they cannot say, and every copy goes.
|
||||
type refetchDuplicateRequest struct {
|
||||
KeepTrackID string `json:"keep_track_id"`
|
||||
}
|
||||
|
||||
// refetchDuplicateResponse reports what went. Blocked names the releases set
|
||||
// aside; empty when the album came from before Minstrel downloaded anything.
|
||||
type refetchDuplicateResponse struct {
|
||||
KeptTrackID *string `json:"kept_track_id"`
|
||||
RemovedPaths []string `json:"removed_paths"`
|
||||
Blocked []string `json:"blocked"`
|
||||
}
|
||||
|
||||
// handleRefetchDuplicateGroup implements POST
|
||||
// /api/admin/library/duplicates/{id}/refetch (#6184): settle a group of one
|
||||
// recording in several slots of an album the way the resolver would, with the
|
||||
// operator's answer. The copies in slots that are not the recording's are
|
||||
// removed and marked missing, the release they came from is set aside, and the
|
||||
// album is wanted again. Merge is the wrong action there: it keeps one title
|
||||
// whether or not it is the audio's, and fetches nothing.
|
||||
//
|
||||
// Errors:
|
||||
// - 409 library_not_writable / 500 file_delete_failed when a file could not be
|
||||
// removed (fileRemoveAPIError)
|
||||
// - 404 duplicate_group_not_pending when the group was already resolved
|
||||
// - 400 not_wrong_slot when the group is not one recording in several slots
|
||||
// - 400 survivor_not_in_group, invalid_id, invalid_body
|
||||
func (h *handlers) handleRefetchDuplicateGroup(w http.ResponseWriter, r *http.Request) {
|
||||
admin, ok := requireUser(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
groupID, ok := parseUUID(chi.URLParam(r, "id"))
|
||||
if !ok {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
var body refetchDuplicateRequest
|
||||
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, mergeRequestBodyLimit)).Decode(&body); err != nil && !errors.Is(err, io.EOF) {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_body")
|
||||
return
|
||||
}
|
||||
var keepID pgtype.UUID // invalid: every copy goes
|
||||
if body.KeepTrackID != "" {
|
||||
if keepID, ok = parseUUID(body.KeepTrackID); !ok {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
res, err := library.SettleWrongSlot(r.Context(), h.pool, h.logger, groupID, keepID, admin.ID)
|
||||
if err != nil {
|
||||
if apiErr, ok := fileRemoveAPIError(err); ok {
|
||||
logFileRemoveFailure(h.logger, apiErr, "group_id", uuidToString(groupID))
|
||||
writeErr(w, apiErr)
|
||||
return
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, library.ErrDuplicateGroupNotPending):
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "duplicate_group_not_pending")
|
||||
case errors.Is(err, library.ErrNotWrongSlot):
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "not_wrong_slot")
|
||||
case errors.Is(err, library.ErrSurvivorNotInGroup):
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "survivor_not_in_group")
|
||||
default:
|
||||
h.logger.Error("admin: refetch duplicate group", "group_id", uuidToString(groupID), "err", err)
|
||||
writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
resp := refetchDuplicateResponse{RemovedPaths: make([]string, 0, len(res.Removed)), Blocked: res.Blocked}
|
||||
if resp.Blocked == nil {
|
||||
resp.Blocked = []string{}
|
||||
}
|
||||
if res.Kept != nil {
|
||||
id := uuidToString(res.Kept.TrackID)
|
||||
resp.KeptTrackID = &id
|
||||
}
|
||||
for _, c := range res.Removed {
|
||||
resp.RemovedPaths = append(resp.RemovedPaths, c.FilePath)
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
@@ -333,6 +333,7 @@ func Mount(r chi.Router, pool *pgxpool.Pool, logger *slog.Logger, events *playev
|
||||
admin.Post("/library/duplicates/sweep", h.handleRunDuplicateSweep)
|
||||
admin.Post("/library/duplicates/{id}/dismiss", h.handleDismissDuplicateGroup)
|
||||
admin.Post("/library/duplicates/{id}/merge", h.handleMergeDuplicateGroup)
|
||||
admin.Post("/library/duplicates/{id}/refetch", h.handleRefetchDuplicateGroup)
|
||||
|
||||
admin.Get("/invites", h.handleListInvites)
|
||||
admin.Post("/invites", h.handleCreateInvite)
|
||||
|
||||
Reference in New Issue
Block a user