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)