fix(config): wire DataDir from yaml/env through to playlists service

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.
This commit is contained in:
2026-05-03 11:37:41 -04:00
parent b0a928c554
commit 6d1709caff
3 changed files with 30 additions and 3 deletions
+11 -1
View File
@@ -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(),
+17
View File
@@ -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 <DataDir>/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
}
+2 -2
View File
@@ -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 {