From bd4f5d381842a95e9cd10c53c912d4481498ca3e Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Wed, 6 May 2026 23:29:27 -0400 Subject: [PATCH] feat(server/coverart): album sidecar falls back to managed cache MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When the album sidecar location isn't writable (e.g. read-only music mount in a container), the album-cover write previously failed and left cover_art_source NULL forever — every scan retried the same provider call and re-failed at the same os.WriteFile. With a managed data_dir available, we fall back to /album-art// cover.jpg and record that path on the album row. The cover-serving HTTP path already serves whatever cover_art_path stores, so the client sees the art uniformly regardless of where it landed. DataDir is opt-in via the Enricher field (cmd/minstrel/main.go sets it from cfg.Storage.DataDir on the production path; tests leave it empty and behave as before). --- internal/coverart/enricher.go | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/internal/coverart/enricher.go b/internal/coverart/enricher.go index a3ad2893..d17940eb 100644 --- a/internal/coverart/enricher.go +++ b/internal/coverart/enricher.go @@ -19,10 +19,17 @@ import ( // for one or many albums. Owns no DB pool itself; receives one per // call so the boot backfill, post-scan trigger, and admin endpoints // share state. +// +// DataDir is the optional managed-cache root. When set, sidecar writes +// that fail (e.g. read-only music mount) fall back to +// /album-art//cover.jpg so enrichment isn't gated +// on the music directory being writable. Empty disables fallback; +// tests use empty since they operate on writable tempdirs. type Enricher struct { pool *pgxpool.Pool logger *slog.Logger settings *SettingsService + DataDir string } // NewEnricher constructs an Enricher. @@ -103,7 +110,22 @@ func (e *Enricher) EnrichAlbum(ctx context.Context, albumID pgtype.UUID) error { } albumDir := filepath.Dir(*row.TrackFilePath) dest := filepath.Join(albumDir, "cover.jpg") - if werr := os.WriteFile(dest, body, 0o644); werr != nil { + werr := os.WriteFile(dest, body, 0o644) + if werr != nil && e.DataDir != "" { + // Sidecar location isn't writable (e.g. RO music mount). + // Fall back to the managed cache so enrichment isn't + // gated on the library being writable. + fallbackDir := filepath.Join(e.DataDir, "album-art", uuidString(albumID)) + if mkErr := os.MkdirAll(fallbackDir, 0o755); mkErr != nil { + e.logger.Error("coverart: managed-cache mkdir failed; leaving NULL", + "album_id", uuidString(albumID), "fallback_dir", fallbackDir, + "sidecar_err", werr, "mkdir_err", mkErr) + return nil + } + dest = filepath.Join(fallbackDir, "cover.jpg") + werr = os.WriteFile(dest, body, 0o644) + } + if werr != nil { e.logger.Error("coverart: write file failed; leaving NULL", "album_id", uuidString(albumID), "path", dest, "provider", provider.ID(), "err", werr)