feat(server/coverart): album sidecar falls back to managed cache

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 <DataDir>/album-art/<album_id>/
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).
This commit is contained in:
2026-05-06 23:29:27 -04:00
parent 5207fae653
commit bd4f5d3818
+23 -1
View File
@@ -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
// <DataDir>/album-art/<album_id>/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)