Add album view and bulk actions to Review
The Review list can now be grouped by album (disc folders folded in), showing per-album counts of upgrades, identical files, downgrades and approximate matches. Whole albums or individual songs can be selected and linked, promoted, ignored or un-ignored in one action. Every bulk action is previewed server-side first: how many songs will be processed, which are skipped and why, and warnings with examples for downgrades, approximate matches, duration mismatches, folder moves and renames. Downgrades are excluded unless explicitly included, and replacing master files requires an acknowledgement. Link and promote run as a background job that re-plans each song and uses the normal single-item operation, so all preflight checks still apply. IDs from another pair are refused and duplicates are ignored. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
0ae4e85bd2
commit
1d31999793
9 files changed
+1209
-7
No files matched your search
+69
-2
@@ -84,6 +84,9 @@ func (s *Server) Handler() http.Handler {
|
||||
r.Post("/job/cancel", s.handleCancelJob)
|
||||
r.Get("/matches", s.handleMatches)
|
||||
r.Get("/export.csv", s.handleExport)
|
||||
r.Get("/groups", s.handleGroups)
|
||||
r.Post("/bulk/preview", s.handleBulkPreview)
|
||||
r.Post("/bulk", s.handleBulk)
|
||||
r.Post("/link-identical", s.handleLinkIdentical)
|
||||
r.Get("/tree", s.handleTree)
|
||||
r.Post("/build", s.handleBuild)
|
||||
@@ -172,7 +175,8 @@ func opErr(w http.ResponseWriter, err error) {
|
||||
case errors.Is(err, engine.ErrBusy), errors.Is(err, engine.ErrDowngrade), errors.Is(err, engine.ErrConfirm),
|
||||
errors.Is(err, fsops.ErrChanged), errors.Is(err, fsops.ErrExists):
|
||||
writeErr(w, 409, err)
|
||||
case errors.Is(err, fsops.ErrOutsideRoot), errors.Is(err, fsops.ErrNotRegular), errors.Is(err, fsops.ErrCrossDevice):
|
||||
case errors.Is(err, fsops.ErrOutsideRoot), errors.Is(err, fsops.ErrNotRegular), errors.Is(err, fsops.ErrCrossDevice),
|
||||
errors.Is(err, engine.ErrInvalidBulk):
|
||||
writeErr(w, 400, err)
|
||||
default:
|
||||
writeErr(w, 500, err)
|
||||
@@ -418,7 +422,13 @@ func (s *Server) handleMatches(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, _ := strconv.Atoi(q.Get("limit"))
|
||||
offset, _ := strconv.Atoi(q.Get("offset"))
|
||||
ms, total, err := s.st.Matches(r.Context(), id, store.MatchFilter{Status: q.Get("status"), Query: q.Get("q"), Limit: limit, Offset: offset})
|
||||
var ids []int64
|
||||
for _, part := range strings.Split(q.Get("ids"), ",") {
|
||||
if n, err := strconv.ParseInt(strings.TrimSpace(part), 10, 64); err == nil && n > 0 {
|
||||
ids = append(ids, n)
|
||||
}
|
||||
}
|
||||
ms, total, err := s.st.Matches(r.Context(), id, store.MatchFilter{Status: q.Get("status"), Query: q.Get("q"), IDs: ids, Limit: limit, Offset: offset})
|
||||
if err != nil {
|
||||
writeErr(w, 500, err)
|
||||
return
|
||||
@@ -567,3 +577,60 @@ func (s *Server) handlePurge(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"ok": true})
|
||||
}
|
||||
|
||||
// ---- album grouping and bulk actions ----
|
||||
|
||||
func (s *Server) handleGroups(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := idParam(r)
|
||||
q := r.URL.Query()
|
||||
groups, err := s.eng.Groups(r.Context(), id, store.MatchFilter{Status: q.Get("status"), Query: q.Get("q")})
|
||||
if err != nil {
|
||||
writeErr(w, 500, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, groups)
|
||||
}
|
||||
|
||||
type bulkBody struct {
|
||||
engine.BulkOptions
|
||||
IDs []int64 `json:"ids"`
|
||||
}
|
||||
|
||||
func (s *Server) handleBulkPreview(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := idParam(r)
|
||||
var b bulkBody
|
||||
if !readJSON(w, r, &b) {
|
||||
return
|
||||
}
|
||||
p, err := s.eng.PreviewBulk(r.Context(), id, b.IDs, b.BulkOptions)
|
||||
if err != nil {
|
||||
writeErr(w, 400, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, p)
|
||||
}
|
||||
|
||||
func (s *Server) handleBulk(w http.ResponseWriter, r *http.Request) {
|
||||
id, _ := idParam(r)
|
||||
var b bulkBody
|
||||
if !readJSON(w, r, &b) {
|
||||
return
|
||||
}
|
||||
switch b.Action {
|
||||
case engine.BulkIgnore, engine.BulkUnignore:
|
||||
n, err := s.eng.ApplyIgnore(r.Context(), id, b.IDs, b.BulkOptions)
|
||||
if err != nil {
|
||||
writeErr(w, 400, err)
|
||||
return
|
||||
}
|
||||
writeJSON(w, 200, map[string]any{"changed": n})
|
||||
default:
|
||||
j, err := s.eng.StartBulk(id, b.IDs, b.BulkOptions)
|
||||
if err != nil {
|
||||
opErr(w, err)
|
||||
return
|
||||
}
|
||||
s.log.Info("bulk action started", "pair", id, "action", b.Action, "songs", len(b.IDs), "allow_downgrade", b.AllowDowngrade)
|
||||
writeJSON(w, 202, j)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user