From 6d1709caffff1ec99b773dcc6dec049997d7c3e0 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Sun, 3 May 2026 11:37:41 -0400 Subject: [PATCH] fix(config): wire DataDir from yaml/env through to playlists service MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Slice 1 of M7 #352 added Server.DataDir but left it un-populated — the playlist cover-collage writer would have used "" as the relative path root in production, writing to the current working directory. - Add Storage.DataDir to Config (yaml: storage.data_dir, env: SMARTMUSIC_STORAGE_DATA_DIR), defaulting to "./data". - server.New takes the data dir as a parameter; main.go threads it. - main.go MkdirAll's the directory at startup so the playlist cover writer doesn't fail on first use with a missing parent. - config.example.yaml documents the new section. Existing internal/server/server_test.go constructs Server directly without server.New, so no test fixture changes needed for that file. --- cmd/minstrel/main.go | 12 +++++++++++- internal/config/config.go | 17 +++++++++++++++++ internal/server/server.go | 4 ++-- 3 files changed, 30 insertions(+), 3 deletions(-) diff --git a/cmd/minstrel/main.go b/cmd/minstrel/main.go index e35670e8..bb324ff7 100644 --- a/cmd/minstrel/main.go +++ b/cmd/minstrel/main.go @@ -97,9 +97,19 @@ func run() error { lidarrReconciler := lidarrrequests.NewReconciler(pool, lidarrCfg, logger.With("component", "lidarr")) go lidarrReconciler.Run(ctx) + // Ensure DataDir exists before any service tries to write into it. + // Best-effort: log + continue on failure; the playlist cover writer + // will surface a clearer error at first use. + if cfg.Storage.DataDir != "" { + if err := os.MkdirAll(cfg.Storage.DataDir, 0o755); err != nil { + logger.Warn("data_dir create failed; cached artifacts may not persist", + "path", cfg.Storage.DataDir, "err", err) + } + } + srv := server.New(logger, pool, scanner, subsonic.Config{ AllowPlaintextPassword: cfg.Subsonic.AllowPlaintextPassword, - }, cfg.Events, cfg.Recommendation) + }, cfg.Events, cfg.Recommendation, cfg.Storage.DataDir) httpServer := &http.Server{ Addr: cfg.Server.Address, Handler: srv.Router(), diff --git a/internal/config/config.go b/internal/config/config.go index 03a34856..f9ddb85f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -11,6 +11,7 @@ import ( type Config struct { Server ServerConfig `yaml:"server"` + Storage StorageConfig `yaml:"storage"` Database DatabaseConfig `yaml:"database"` Log LogConfig `yaml:"log"` Auth AuthConfig `yaml:"auth"` @@ -24,6 +25,18 @@ type ServerConfig struct { Address string `yaml:"address"` } +// StorageConfig points at on-disk locations the server uses for cached +// runtime artifacts. Currently: +// - DataDir hosts /playlist_covers/{playlist_id}.jpg (M7 #352). +// More entries may grow here (e.g., transcode cache) without breaking +// existing layouts. +type StorageConfig struct { + // DataDir is the root for cached artifacts. Default: "./data" (relative + // to the working directory). Operators in production should set an + // absolute path persistent across restarts. + DataDir string `yaml:"data_dir"` +} + type DatabaseConfig struct { URL string `yaml:"url"` } @@ -82,6 +95,7 @@ type RecommendationConfig struct { func Default() Config { return Config{ Server: ServerConfig{Address: ":4533"}, + Storage: StorageConfig{DataDir: "./data"}, Database: DatabaseConfig{URL: ""}, Log: LogConfig{Level: "info", Format: "text"}, Events: EventsConfig{ @@ -124,6 +138,9 @@ func applyEnv(cfg *Config) { if v, ok := os.LookupEnv("SMARTMUSIC_SERVER_ADDRESS"); ok { cfg.Server.Address = v } + if v, ok := os.LookupEnv("SMARTMUSIC_STORAGE_DATA_DIR"); ok { + cfg.Storage.DataDir = v + } if v, ok := os.LookupEnv("SMARTMUSIC_DATABASE_URL"); ok { cfg.Database.URL = v } diff --git a/internal/server/server.go b/internal/server/server.go index 2ffc0893..8819920f 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -70,8 +70,8 @@ type Server struct { DataDir string } -func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig) *Server { - return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg} +func New(logger *slog.Logger, pool *pgxpool.Pool, scanner ScanTrigger, subCfg subsonic.Config, eventsCfg config.EventsConfig, recCfg config.RecommendationConfig, dataDir string) *Server { + return &Server{Logger: logger, Pool: pool, Scanner: scanner, SubsonicCfg: subCfg, EventsCfg: eventsCfg, RecommendationCfg: recCfg, DataDir: dataDir} } func (s *Server) Router() http.Handler {