diff --git a/cmd/minstrel/main.go b/cmd/minstrel/main.go index bc3aa891..767d1bac 100644 --- a/cmd/minstrel/main.go +++ b/cmd/minstrel/main.go @@ -100,8 +100,10 @@ func run() error { if _, err := library.RunScan(ctx, pool, scanner, coverEnricher, logger.With("component", "scan_run"), library.RunScanConfig{ - BackfillCap: 5000, - EnrichCap: cfg.Library.CoverArtBackfillCap, + BackfillCap: 5000, + EnrichCap: cfg.Library.CoverArtBackfillCap, + ArtistEnrichCap: cfg.Library.CoverArtBackfillCap, + DataDir: cfg.Storage.DataDir, }, ); err != nil { logger.Warn("startup scan run failed", "err", err) @@ -115,8 +117,10 @@ func run() error { if _, err := library.RunScan(ctx, pool, nil, coverEnricher, logger.With("component", "scan_run"), library.RunScanConfig{ - BackfillCap: 5000, - EnrichCap: cfg.Library.CoverArtBackfillCap, + BackfillCap: 5000, + EnrichCap: cfg.Library.CoverArtBackfillCap, + ArtistEnrichCap: cfg.Library.CoverArtBackfillCap, + DataDir: cfg.Storage.DataDir, }, ); err != nil { logger.Warn("boot-only scan run failed", "err", err) @@ -154,8 +158,10 @@ func run() error { } scanCfg := library.RunScanConfig{ - BackfillCap: 5000, - EnrichCap: cfg.Library.CoverArtBackfillCap, + BackfillCap: 5000, + EnrichCap: cfg.Library.CoverArtBackfillCap, + ArtistEnrichCap: cfg.Library.CoverArtBackfillCap, + DataDir: cfg.Storage.DataDir, } srv := server.New(logger, pool, scanner, subsonic.Config{ AllowPlaintextPassword: cfg.Subsonic.AllowPlaintextPassword, diff --git a/internal/api/admin_scan.go b/internal/api/admin_scan.go index e4736825..e1ed856c 100644 --- a/internal/api/admin_scan.go +++ b/internal/api/admin_scan.go @@ -17,14 +17,15 @@ import ( // rather than base64 byte arrays — the DB column is bytea-shaped jsonb in // pgx but the data is already valid JSON. type scanStatusResp struct { - ID string `json:"id"` - StartedAt string `json:"started_at"` - FinishedAt *string `json:"finished_at"` - Library json.RawMessage `json:"library,omitempty"` - MbidBackfill json.RawMessage `json:"mbid_backfill,omitempty"` - CoverEnrich json.RawMessage `json:"cover_enrich,omitempty"` - ErrorMessage *string `json:"error_message,omitempty"` - InFlight bool `json:"in_flight"` + ID string `json:"id"` + StartedAt string `json:"started_at"` + FinishedAt *string `json:"finished_at"` + Library json.RawMessage `json:"library,omitempty"` + MbidBackfill json.RawMessage `json:"mbid_backfill,omitempty"` + CoverEnrich json.RawMessage `json:"cover_enrich,omitempty"` + ArtistArtEnrich json.RawMessage `json:"artist_art_enrich,omitempty"` + ErrorMessage *string `json:"error_message,omitempty"` + InFlight bool `json:"in_flight"` } // handleGetScanStatus implements GET /api/admin/scan/status. @@ -62,6 +63,9 @@ func (h *handlers) handleGetScanStatus(w http.ResponseWriter, r *http.Request) { if len(row.CoverEnrich) > 0 { resp.CoverEnrich = row.CoverEnrich } + if len(row.ArtistArtEnrich) > 0 { + resp.ArtistArtEnrich = row.ArtistArtEnrich + } writeJSON(w, http.StatusOK, resp) } diff --git a/internal/db/dbq/scan_runs.sql.go b/internal/db/dbq/scan_runs.sql.go index a21e0ad2..94cb53bd 100644 --- a/internal/db/dbq/scan_runs.sql.go +++ b/internal/db/dbq/scan_runs.sql.go @@ -53,20 +53,21 @@ func (q *Queries) GetInFlightScanRun(ctx context.Context) (GetInFlightScanRunRow } const getLatestScanRun = `-- name: GetLatestScanRun :one -SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, error_message +SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, artist_art_enrich, error_message FROM scan_runs ORDER BY started_at DESC LIMIT 1 ` type GetLatestScanRunRow struct { - ID pgtype.UUID - StartedAt pgtype.Timestamptz - FinishedAt pgtype.Timestamptz - Library []byte - MbidBackfill []byte - CoverEnrich []byte - ErrorMessage *string + ID pgtype.UUID + StartedAt pgtype.Timestamptz + FinishedAt pgtype.Timestamptz + Library []byte + MbidBackfill []byte + CoverEnrich []byte + ArtistArtEnrich []byte + ErrorMessage *string } // Powers GET /api/admin/scan/status. Returns NoRows when no scan has @@ -81,6 +82,7 @@ func (q *Queries) GetLatestScanRun(ctx context.Context) (GetLatestScanRunRow, er &i.Library, &i.MbidBackfill, &i.CoverEnrich, + &i.ArtistArtEnrich, &i.ErrorMessage, ) return i, err @@ -106,6 +108,23 @@ func (q *Queries) InsertScanRun(ctx context.Context) (InsertScanRunRow, error) { return i, err } +const updateScanRunArtistArtEnrich = `-- name: UpdateScanRunArtistArtEnrich :exec +UPDATE scan_runs + SET artist_art_enrich = $2 + WHERE id = $1 +` + +type UpdateScanRunArtistArtEnrichParams struct { + ID pgtype.UUID + ArtistArtEnrich []byte +} + +// M7 cover-sources: persists the artist-art enrich stage tally JSON. +func (q *Queries) UpdateScanRunArtistArtEnrich(ctx context.Context, arg UpdateScanRunArtistArtEnrichParams) error { + _, err := q.db.Exec(ctx, updateScanRunArtistArtEnrich, arg.ID, arg.ArtistArtEnrich) + return err +} + const updateScanRunCoverEnrich = `-- name: UpdateScanRunCoverEnrich :exec UPDATE scan_runs SET cover_enrich = $2 WHERE id = $1 ` diff --git a/internal/db/queries/scan_runs.sql b/internal/db/queries/scan_runs.sql index 1ef24825..c2a005df 100644 --- a/internal/db/queries/scan_runs.sql +++ b/internal/db/queries/scan_runs.sql @@ -27,11 +27,17 @@ UPDATE scan_runs -- name: GetLatestScanRun :one -- Powers GET /api/admin/scan/status. Returns NoRows when no scan has -- ever run; the API layer maps that to a 200 with empty payload. -SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, error_message +SELECT id, started_at, finished_at, library, mbid_backfill, cover_enrich, artist_art_enrich, error_message FROM scan_runs ORDER BY started_at DESC LIMIT 1; +-- name: UpdateScanRunArtistArtEnrich :exec +-- M7 cover-sources: persists the artist-art enrich stage tally JSON. +UPDATE scan_runs + SET artist_art_enrich = $2 + WHERE id = $1; + -- name: GetInFlightScanRun :one -- Used by POST /api/admin/scan/run to 409 when a scan is already running. -- "In flight" = finished_at IS NULL. diff --git a/internal/library/scanrun.go b/internal/library/scanrun.go index a27b7f56..5e2fe5b2 100644 --- a/internal/library/scanrun.go +++ b/internal/library/scanrun.go @@ -40,12 +40,22 @@ type CoverEnrichStageTallies struct { Failed int `json:"failed"` } +// ArtistArtEnrichStageTallies wires EnrichArtistBatch tallies into +// the scan_runs jsonb column. +type ArtistArtEnrichStageTallies struct { + Processed int `json:"processed"` + Succeeded int `json:"succeeded"` + Failed int `json:"failed"` +} + // RunScanConfig assembles the knobs for the orchestrator. BackfillCap and // EnrichCap mirror the existing boot-time caps (5000 / coverArtBackfillCap) // so manual triggers don't accidentally drain the whole library. type RunScanConfig struct { - BackfillCap int - EnrichCap int + BackfillCap int + EnrichCap int + ArtistEnrichCap int // M7 cover-sources: cap for the artist-art enrich stage + DataDir string // M7 cover-sources: where to write artist-art files } // RunScan is the unified scan-chain orchestrator. Creates a scan_runs row, @@ -131,6 +141,22 @@ func RunScan( } } + // Stage 4: artist art enrichment. + if enricher != nil && cfg.ArtistEnrichCap != 0 && cfg.DataDir != "" { + processed, succeeded, failed, ceerr := enricher.EnrichArtistBatch(ctx, cfg.ArtistEnrichCap, cfg.DataDir) + if ceerr != nil { + captureErr("artist_art_enrich", ceerr) + } + blob, _ := json.Marshal(ArtistArtEnrichStageTallies{ + Processed: processed, Succeeded: succeeded, Failed: failed, + }) + if uerr := q.UpdateScanRunArtistArtEnrich(ctx, dbq.UpdateScanRunArtistArtEnrichParams{ + ID: row.ID, ArtistArtEnrich: blob, + }); uerr != nil { + captureErr("artist_art_enrich_persist", uerr) + } + } + // Finalize: set finished_at + error message (or empty string if all stages succeeded). errMsg := "" if firstErr != nil { diff --git a/web/src/lib/api/admin.ts b/web/src/lib/api/admin.ts index 91b2620d..0aebaabd 100644 --- a/web/src/lib/api/admin.ts +++ b/web/src/lib/api/admin.ts @@ -205,6 +205,12 @@ export type ScanStageCoverEnrich = { failed: number; }; +export type ScanStageArtistArtEnrich = { + processed: number; + succeeded: number; + failed: number; +}; + export type ScanStatus = { id: string; started_at: string; @@ -212,6 +218,7 @@ export type ScanStatus = { library?: ScanStageLibrary; mbid_backfill?: ScanStageMbidBackfill; cover_enrich?: ScanStageCoverEnrich; + artist_art_enrich?: ScanStageArtistArtEnrich; error_message?: string; in_flight: boolean; }; diff --git a/web/src/routes/admin/+page.svelte b/web/src/routes/admin/+page.svelte index 129237c7..6bb1165b 100644 --- a/web/src/routes/admin/+page.svelte +++ b/web/src/routes/admin/+page.svelte @@ -343,7 +343,7 @@ {#if !scan || !scan.id}
No scan has run yet.
{:else} -{scanInFlight ? 'Pending…' : 'Skipped.'}
{/if}{scanInFlight ? 'Pending…' : 'Skipped.'}
+ {/if} +