feat(server/coverart): batch summary log with category breakdown

The existing processed/succeeded/failed tally written to scan_runs
collapses too much to debug "0 successes" symptoms — failed includes
"settled to none (correct)" alongside "left NULL (transient/IO error,
will retry)" alongside "skipped (no MBID)". An operator looking at
{"failed": 144, "succeeded": 0} can't tell whether the enricher is
broken, the upstream is broken, the data is missing MBIDs, or the
artists genuinely aren't in TheAudioDB.

Adds a single end-of-batch Info log per stage with the breakdown:
  - eligible:    rows returned by the SQL filter
  - processed:   rows the loop actually touched
  - succeeded:   art attached (provider success)
  - settled_none: tried, no provider had art (legit miss)
  - no_mbid:     skipped inside the enricher because MBID was NULL
                 (artist stage only)
  - left_null:   tried, transient/IO failure, will retry next pass
  - errored:     internal error during the entry (logged separately)

The JSON written to scan_runs keeps its existing shape so the admin
UI's stage badges aren't disturbed.
This commit is contained in:
2026-05-06 23:30:21 -04:00
parent bd4f5d3818
commit 126d4e53aa
2 changed files with 70 additions and 6 deletions
+36 -3
View File
@@ -135,6 +135,12 @@ func (e *Enricher) recordArtistArt(ctx context.Context, artistID pgtype.UUID, th
// EnrichArtistBatch drains up to limit artists whose eligibility loop
// determines they need processing. Iterates serially. Returns
// per-batch tallies for the orchestrator's scan-run record.
//
// Emits a single Info log line at end-of-batch with a category
// breakdown (succeeded / settled-none / no-MBID / left-NULL /
// errored). The JSON tally written to scan_runs keeps the existing
// processed/succeeded/failed shape for backwards compat with the
// admin overview UI; the log line is the operator's diagnostic.
func (e *Enricher) EnrichArtistBatch(ctx context.Context, limit int, dataDir string,
progressCb func(processed, succeeded, failed int)) (processed, succeeded, failed int, err error) {
if limit <= 0 {
@@ -148,8 +154,10 @@ func (e *Enricher) EnrichArtistBatch(ctx context.Context, limit int, dataDir str
if qerr != nil {
return 0, 0, 0, fmt.Errorf("list missing artist art: %w", qerr)
}
var settledNone, noMBID, leftNull, errored int
for _, id := range rows {
if ctx.Err() != nil {
e.logArtistBatchSummary(len(rows), processed, succeeded, settledNone, noMBID, leftNull, errored)
return processed, succeeded, failed, ctx.Err()
}
processed++
@@ -157,6 +165,7 @@ func (e *Enricher) EnrichArtistBatch(ctx context.Context, limit int, dataDir str
e.logger.Warn("coverart: artist batch entry failed",
"artist_id", uuidString(id), "err", eerr)
failed++
errored++
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
@@ -166,25 +175,49 @@ func (e *Enricher) EnrichArtistBatch(ctx context.Context, limit int, dataDir str
row, rerr := q.GetArtistByID(ctx, id)
if rerr != nil {
failed++
errored++
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
continue
}
if row.ArtistArtSource != nil &&
switch {
case row.ArtistArtSource != nil &&
*row.ArtistArtSource != "" &&
*row.ArtistArtSource != "none" {
*row.ArtistArtSource != "none":
succeeded++
} else {
case row.ArtistArtSource != nil && *row.ArtistArtSource == "none":
settledNone++
failed++
case row.Mbid == nil || *row.Mbid == "":
noMBID++
failed++
default:
leftNull++
failed++
}
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
}
e.logArtistBatchSummary(len(rows), processed, succeeded, settledNone, noMBID, leftNull, errored)
return processed, succeeded, failed, nil
}
// logArtistBatchSummary emits a single Info line at end-of-batch with
// the full category breakdown including MBID-skipped (artists eligible
// by the SQL filter but skipped inside EnrichArtist due to NULL MBID).
func (e *Enricher) logArtistBatchSummary(eligible, processed, succeeded, settledNone, noMBID, leftNull, errored int) {
e.logger.Info("coverart: artist enrichment batch complete",
"eligible", eligible,
"processed", processed,
"succeeded", succeeded,
"settled_none", settledNone,
"no_mbid", noMBID,
"left_null", leftNull,
"errored", errored)
}
// CleanupArtistArt removes the artist-art directory for a deleted
// artist. Idempotent — no-op if the directory doesn't exist. Called
// from the artist-delete cascade in tracks/service.go after the
+34 -3
View File
@@ -168,6 +168,12 @@ func (e *Enricher) recordAlbumCover(ctx context.Context, albumID pgtype.UUID, pa
// EnrichBatch drains up to limit albums whose eligibility loop
// determines they need processing. Iterates serially. Returns
// per-batch tallies for the orchestrator's scan-run record.
//
// Emits a single Info log line at end-of-batch with a category
// breakdown (succeeded / settled-none / left-NULL / errored). The
// JSON tally written to scan_runs keeps the existing
// processed/succeeded/failed shape for backwards compat with the
// admin overview UI; the log line is the operator's diagnostic.
func (e *Enricher) EnrichBatch(ctx context.Context, limit int,
progressCb func(processed, succeeded, failed int)) (processed, succeeded, failed int, err error) {
if limit <= 0 {
@@ -181,8 +187,10 @@ func (e *Enricher) EnrichBatch(ctx context.Context, limit int,
if qerr != nil {
return 0, 0, 0, fmt.Errorf("list missing: %w", qerr)
}
var settledNone, leftNull, errored int
for _, id := range rows {
if ctx.Err() != nil {
e.logBatchSummary("album", len(rows), processed, succeeded, settledNone, leftNull, errored)
return processed, succeeded, failed, ctx.Err()
}
processed++
@@ -190,6 +198,7 @@ func (e *Enricher) EnrichBatch(ctx context.Context, limit int,
e.logger.Warn("coverart: batch entry failed",
"album_id", uuidString(id), "err", eerr)
failed++
errored++
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
@@ -199,27 +208,49 @@ func (e *Enricher) EnrichBatch(ctx context.Context, limit int,
row, rerr := q.GetAlbumWithFirstTrackPath(ctx, id)
if rerr != nil {
failed++
errored++
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
continue
}
if row.CoverArtSource != nil &&
switch {
case row.CoverArtSource != nil &&
(*row.CoverArtSource == "sidecar" ||
*row.CoverArtSource == "mbcaa" ||
*row.CoverArtSource == "embedded" ||
*row.CoverArtSource == "theaudiodb") {
*row.CoverArtSource == "theaudiodb"):
succeeded++
} else {
case row.CoverArtSource != nil && *row.CoverArtSource == "none":
settledNone++
failed++
default:
leftNull++
failed++
}
if progressCb != nil {
progressCb(processed, succeeded, failed)
}
}
e.logBatchSummary("album", len(rows), processed, succeeded, settledNone, leftNull, errored)
return processed, succeeded, failed, nil
}
// logBatchSummary emits a single Info line at end-of-batch with the
// full category breakdown. Operator-side diagnostic; the
// processed/succeeded/failed tally collapses too much to debug
// "0 successes" symptoms by itself.
func (e *Enricher) logBatchSummary(stage string, eligible, processed, succeeded, settledNone, leftNull, errored int) {
e.logger.Info("coverart: enrichment batch complete",
"stage", stage,
"eligible", eligible,
"processed", processed,
"succeeded", succeeded,
"settled_none", settledNone,
"left_null", leftNull,
"errored", errored)
}
// EnrichRetryMissing drains both NULL and 'none' rows. Used by the
// admin bulk-retry endpoint. Clears 'none' to NULL first so
// EnrichAlbum picks them up.