From 126d4e53aa6da675f92a60756d419bac3be64c59 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 6 May 2026 23:30:21 -0400 Subject: [PATCH] feat(server/coverart): batch summary log with category breakdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- internal/coverart/artist_enricher.go | 39 +++++++++++++++++++++++++--- internal/coverart/enricher.go | 37 +++++++++++++++++++++++--- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/internal/coverart/artist_enricher.go b/internal/coverart/artist_enricher.go index 1771793a..0dc2f027 100644 --- a/internal/coverart/artist_enricher.go +++ b/internal/coverart/artist_enricher.go @@ -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 diff --git a/internal/coverart/enricher.go b/internal/coverart/enricher.go index d17940eb..ae9c7970 100644 --- a/internal/coverart/enricher.go +++ b/internal/coverart/enricher.go @@ -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.