test-go / test (push) Failing after 44s
test-web / test (push) Successful in 49s
test-go / integration (push) Failing after 2m42s
release / Build + push container image (push) Canceled after 0s
release / Verify release artifacts (tag releases only) (push) Canceled after 0s
release / Build signed APK (releases and dev) (push) Canceled after 4m8s
Rule 25: the fingerprinting knobs move out of source into a DB-backed singleton (migration 0061), edited from a card on the Duplicates page and shared live with the scanner, the backfill and the duplicate sweep through one service instance, so a save needs no restart. The length is the knob that can silently break the library: prints taken at two lengths never match. Each track_fingerprints row now records the length it was taken at, and every reader filters on the current one — the backfill treats another length as stale, the gauge counts it pending, the sweep never streams it. Equivalent to a version bump, except that setting the length back makes rows not yet redone current again. The card warns before a length change re-fingerprints the library. Off stops every decode: the scan takes only the stream hash (a demux, and what recognises a moved file) and stores nothing, dropping a changed file's stale row; the backfill idles. A save also makes a sweep due, since a new threshold or length changes what the same prints group into, and the sweep interval gains slack so an hourly interval on an hourly tick doesn't skip every other tick. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01SQ31KQpYbStyK5y58UmPLH
84 lines
3.1 KiB
Go
84 lines
3.1 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/apierror"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/library"
|
|
)
|
|
|
|
// coverageRollupResp is the wire shape for GET /api/admin/library/coverage.
|
|
// All fields are non-negative counts. with_art + pending + settled = total;
|
|
// pending_no_mbid is a subset of pending.
|
|
type coverageRollupResp struct {
|
|
Total int64 `json:"total"`
|
|
WithArt int64 `json:"with_art"`
|
|
Pending int64 `json:"pending"`
|
|
Settled int64 `json:"settled"`
|
|
PendingNoMbid int64 `json:"pending_no_mbid"`
|
|
}
|
|
|
|
// handleGetLibraryCoverage implements GET /api/admin/library/coverage.
|
|
// Surfaces a library-wide rollup of albums.cover_art_source for the admin
|
|
// dashboard gauge. Always 200; zeros on an empty library.
|
|
func (h *handlers) handleGetLibraryCoverage(w http.ResponseWriter, r *http.Request) {
|
|
q := dbq.New(h.pool)
|
|
row, err := q.GetAlbumCoverageRollup(r.Context())
|
|
if err != nil {
|
|
writeErrWithLog(w, h.logger, "admin: get coverage rollup", apierror.InternalMsg("lookup failed", err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, coverageRollupResp{
|
|
Total: row.Total,
|
|
WithArt: row.WithArt,
|
|
Pending: row.Pending,
|
|
Settled: row.Settled,
|
|
PendingNoMbid: row.PendingNoMbid,
|
|
})
|
|
}
|
|
|
|
// fingerprintCoverageResp is the wire shape for GET /api/admin/library/fingerprints.
|
|
// fingerprinted + rejected + pending = total. Missing tracks are not counted:
|
|
// there is no file to fingerprint. Enabled travels with the counts because with
|
|
// fingerprinting off (#3913) pending never shrinks, and a gauge that implies
|
|
// progress would be promising work nothing is doing.
|
|
type fingerprintCoverageResp struct {
|
|
Total int64 `json:"total"`
|
|
Fingerprinted int64 `json:"fingerprinted"`
|
|
Rejected int64 `json:"rejected"`
|
|
Pending int64 `json:"pending"`
|
|
Enabled bool `json:"enabled"`
|
|
}
|
|
|
|
// fingerprintCoverage reads the gauge against the current settings: a print at
|
|
// another length counts as pending, because the backfill will re-derive it.
|
|
func (h *handlers) fingerprintCoverage(ctx context.Context) (fingerprintCoverageResp, error) {
|
|
cfg := h.fingerprintSettings.Get()
|
|
row, err := library.FingerprintCoverage(ctx, h.pool, cfg)
|
|
if err != nil {
|
|
return fingerprintCoverageResp{}, err
|
|
}
|
|
return fingerprintCoverageResp{
|
|
Total: row.Total,
|
|
Fingerprinted: row.Fingerprinted,
|
|
Rejected: row.Rejected,
|
|
Pending: row.Pending,
|
|
Enabled: cfg.Enabled,
|
|
}, nil
|
|
}
|
|
|
|
// handleGetFingerprintCoverage implements GET /api/admin/library/fingerprints:
|
|
// how far the fingerprint backfill (#3908) has got. The backfill is its own
|
|
// worker spanning many passes, with no scan run to attach a tally to, so its
|
|
// progress is read live here. Always 200; zeros on an empty library.
|
|
func (h *handlers) handleGetFingerprintCoverage(w http.ResponseWriter, r *http.Request) {
|
|
cov, err := h.fingerprintCoverage(r.Context())
|
|
if err != nil {
|
|
writeErrWithLog(w, h.logger, "admin: get fingerprint coverage", apierror.InternalMsg("lookup failed", err))
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, cov)
|
|
}
|