feat: video rips and stray copies handle themselves (M498 #5439)
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 26s
release / go (push) Failing after 58s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m25s
release / Build signed APK (releases and dev) (push) Canceled after 3m26s
release / android (push) Canceled after 3m28s
release / govulncheck (push) Successful in 25s
release / web (push) Failing after 26s
release / go (push) Failing after 58s
release / Attach APK to the Release (tag releases only) (push) Canceled after 0s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / integration (push) Canceled after 3m25s
release / Build signed APK (releases and dev) (push) Canceled after 3m26s
release / android (push) Canceled after 3m28s
- The resolver also merges cross-release and mismatch groups where Lidarr
maps exactly one copy: the others fulfil nothing, so removing them opens no
hole (D-a rule 1). That covers a rip beside the clean copy on another
release and a wrong-file import Lidarr holds unmapped. With two or more
mapped copies each fulfils its own release and nothing is removed.
- A track whose file name carries a video-rip marker is held back from radio
and the system mixes (tracks.source_verdict, migration 0077). It still plays
when chosen. A renamed file is released; the operator's "fine" sticks.
- Suspect sources shows what was done to each track, with "This one is fine"
and "Hold back again" (PUT /api/admin/library/suspect-sources/{id}).
- The Liked list prefers a copy that is not held back.
Replacing a rip that has no clean copy is left for the operator to decide.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
||||
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
||||
@@ -22,6 +26,11 @@ type suspectTrackView struct {
|
||||
DiscNumber *int32 `json:"disc_number"`
|
||||
TrackNumber *int32 `json:"track_number"`
|
||||
Markers []string `json:"markers"`
|
||||
// Verdict is "suspect" while the track is held back from radio and the
|
||||
// mixes, "fine" once the operator let it back, null before the resolver's
|
||||
// first pass (M498 #5439). VerdictAt is when it was last set.
|
||||
Verdict *string `json:"verdict"`
|
||||
VerdictAt *string `json:"verdict_at"`
|
||||
}
|
||||
|
||||
// suspectGroupView is a folder's worth of flagged tracks. As on the
|
||||
@@ -42,9 +51,10 @@ type adminSuspectSourcesResponse struct {
|
||||
|
||||
// handleListSuspectSources implements GET /api/admin/library/suspect-sources.
|
||||
//
|
||||
// Read-only. A marker is a reason to look, not proof: a band can title a song
|
||||
// "Music Video". What to do about a flagged file — merge it in Duplicates,
|
||||
// quarantine it, replace it in Lidarr — stays the operator's call.
|
||||
// A marker is a reason to look, not proof: a band can title a song "Music
|
||||
// Video". The duplicate resolver holds a flagged track back from radio and the
|
||||
// mixes and merges it away where a clean copy exists (M498 #5439); this report
|
||||
// is where the operator sees that and can say a track is fine.
|
||||
func (h *handlers) handleListSuspectSources(w http.ResponseWriter, r *http.Request) {
|
||||
limit, offset, err := parsePaging(r.URL.Query())
|
||||
if err != nil {
|
||||
@@ -96,6 +106,11 @@ func groupSuspectByDirectory(rows []dbq.ListSuspectSourceTracksRow) []suspectGro
|
||||
DiscNumber: row.DiscNumber,
|
||||
TrackNumber: row.TrackNumber,
|
||||
Markers: library.SourceMarkersFor(row.FilePath),
|
||||
Verdict: row.SourceVerdict,
|
||||
}
|
||||
if row.SourceVerdictAt.Valid {
|
||||
at := row.SourceVerdictAt.Time.UTC().Format(time.RFC3339)
|
||||
t.VerdictAt = &at
|
||||
}
|
||||
if n := len(groups); n > 0 && groups[n-1].Directory == row.Directory {
|
||||
groups[n-1].Tracks = append(groups[n-1].Tracks, t)
|
||||
@@ -108,3 +123,39 @@ func groupSuspectByDirectory(rows []dbq.ListSuspectSourceTracksRow) []suspectGro
|
||||
}
|
||||
return groups
|
||||
}
|
||||
|
||||
type suspectVerdictRequest struct {
|
||||
Verdict string `json:"verdict"`
|
||||
}
|
||||
|
||||
// handleSetSuspectSourceVerdict implements PUT
|
||||
// /api/admin/library/suspect-sources/{id}: {"verdict": "fine"} lets a flagged
|
||||
// track back into radio and the mixes for good, {"verdict": "suspect"} holds
|
||||
// it back again. 404 suspect_track_not_found when the track was never flagged.
|
||||
func (h *handlers) handleSetSuspectSourceVerdict(w http.ResponseWriter, r *http.Request) {
|
||||
id, ok := parseUUID(chi.URLParam(r, "id"))
|
||||
if !ok {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_id")
|
||||
return
|
||||
}
|
||||
var req suspectVerdictRequest
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_body")
|
||||
return
|
||||
}
|
||||
if req.Verdict != "fine" && req.Verdict != "suspect" {
|
||||
writeAdminJSONErr(w, http.StatusBadRequest, "invalid_verdict")
|
||||
return
|
||||
}
|
||||
n, err := dbq.New(h.pool).SetTrackSourceVerdict(r.Context(), dbq.SetTrackSourceVerdictParams{Verdict: req.Verdict, ID: id})
|
||||
if err != nil {
|
||||
h.logger.Error("admin: set suspect-source verdict", "err", err)
|
||||
writeAdminJSONErr(w, http.StatusInternalServerError, "server_error")
|
||||
return
|
||||
}
|
||||
if n == 0 {
|
||||
writeAdminJSONErr(w, http.StatusNotFound, "suspect_track_not_found")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]string{"verdict": req.Verdict})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user