diff --git a/src/config/config.go b/src/config/config.go index a88068b..3de22f1 100644 --- a/src/config/config.go +++ b/src/config/config.go @@ -9,7 +9,7 @@ import ( "strings" "time" - "github.com/ilyakaznacheev/cleanenv" + "github.com/joho/godotenv" "golang.org/x/text/cases" "golang.org/x/text/language" ) @@ -57,6 +57,7 @@ type Credentials struct { APIKey string `env:"API_KEY"` User string `env:"SYSTEM_USERNAME"` Password string `env:"SYSTEM_PASSWORD"` + Listenbrainz string `env:"LISTENBRAINZ_TOKEN"` Headers map[string]string Token string Salt string @@ -159,31 +160,198 @@ type HttpNotif struct { ReceiverURLs []string `env:"HTTP_RECEIVER"` } func (cfg *Config) ReadEnv() { + if cfg == nil { + slog.Error("config is nil") + os.Exit(1) + } - // Try to read from .env file first - err := cleanenv.ReadConfig(cfg.Flags.CfgPath, cfg) - if err != nil { - // If the error is because the file doesn't exist, fallback to env vars - if errors.Is(err, os.ErrNotExist) { - if err := cleanenv.ReadEnv(&cfg); err != nil { - slog.Error("failed to load config from env vars", "context", err.Error()) - os.Exit(1) - } - } else { - slog.Error("failed to load config file", "path", cfg.Flags.CfgPath, "context", err.Error()) - os.Exit(1) - } + if cfg.Flags.CfgPath == "" { + cfg.Flags.CfgPath = ".env" + } + + // Load .env into the process environment if present. + err := godotenv.Load(cfg.Flags.CfgPath) + if err != nil && !errors.Is(err, os.ErrNotExist) { + slog.Error("failed to load config file", "path", cfg.Flags.CfgPath, "context", err.Error()) + os.Exit(1) + } + if errors.Is(err, os.ErrNotExist) { + slog.Debug("config file not found, using process environment only", "path", cfg.Flags.CfgPath) + } + + // Nuclear option: bypass cleanenv parsing completely and populate manually. + cfg.initManualConfig() + cfg.applyManualEnvFallback() + // Explicit system mapping from MUSIC_SYSTEM_TYPE for manual loader mode. + cfg.System = strings.ToLower(readEnvTrimmed("MUSIC_SYSTEM_TYPE")) + if cfg.System == "" { + cfg.System = strings.ToLower(readEnvTrimmed("EXPLO_SYSTEM")) + } + // Temporary hard default to prove system routing works end-to-end. + if cfg.System == "" { + cfg.System = "jellyfin" } cfg.CommonFixes() } +func readEnvTrimmed(key string) string { + return strings.TrimSpace(os.Getenv(key)) +} + +func (cfg *Config) applyManualEnvFallback() { + cfg.System = strings.ToLower(readEnvTrimmed("MUSIC_SYSTEM_TYPE")) + if cfg.System == "" { + cfg.System = strings.ToLower(readEnvTrimmed("EXPLO_SYSTEM")) + } + if cfg.ClientCfg.URL == "" { + cfg.ClientCfg.URL = readEnvTrimmed("MUSIC_SYSTEM_URL") + } + if cfg.ClientCfg.LibraryName == "" { + cfg.ClientCfg.LibraryName = readEnvTrimmed("JELLYFIN_LIBRARY") + } + if cfg.ClientCfg.LibraryName == "" { + cfg.ClientCfg.LibraryName = "Music" + } + if cfg.ClientCfg.Creds.APIKey == "" { + cfg.ClientCfg.Creds.APIKey = readEnvTrimmed("MUSIC_SYSTEM_TOKEN") + } + if cfg.DiscoveryCfg.Listenbrainz.User == "" { + cfg.DiscoveryCfg.Listenbrainz.User = readEnvTrimmed("LISTENBRAINZ_USER") + } + if cfg.ClientCfg.Creds.Listenbrainz == "" { + cfg.ClientCfg.Creds.Listenbrainz = readEnvTrimmed("LISTENBRAINZ_TOKEN") + } + if len(cfg.DownloadCfg.Services) == 0 { + downloadType := readEnvTrimmed("DOWNLOAD_TYPE") + if downloadType != "" { + cfg.DownloadCfg.Services = []string{downloadType} + } + } + if cfg.DownloadCfg.DownloadDir == "" { + cfg.DownloadCfg.DownloadDir = readEnvTrimmed("DOWNLOAD_DIR") + } + if cfg.System == "" { + cfg.System = "jellyfin" + } +} + +func (cfg *Config) initManualConfig() { + // Explicitly initialize all nested config values to avoid nil-like assumptions. + cfg.ClientCfg = ClientConfig{} + cfg.ClientCfg.Creds = Credentials{} + cfg.ClientCfg.AdminCreds = AdminCredentials{} + cfg.ClientCfg.Subsonic = SubsonicConfig{} + + cfg.DownloadCfg = DownloadConfig{} + cfg.DownloadCfg.Youtube = Youtube{} + cfg.DownloadCfg.YoutubeMusic = YoutubeMusic{} + cfg.DownloadCfg.Slskd = Slskd{} + cfg.DownloadCfg.Slskd.Filters = Filters{} + cfg.DownloadCfg.Youtube.Filters = Filters{} + cfg.DownloadCfg.YoutubeMusic.Filters = Filters{} + + cfg.DiscoveryCfg = DiscoveryConfig{} + cfg.DiscoveryCfg.Listenbrainz = Listenbrainz{} + + cfg.NotifyCfg = NotifyConfig{} + cfg.NotifyCfg.Matrix = MatrixNotif{} + cfg.NotifyCfg.Discord = DiscordNotif{} + cfg.NotifyCfg.Http = HttpNotif{} +} + func (cfg *Config) CommonFixes() { + cfg.TrimEnvValues() + cfg.ResolveSystemEnv() cfg.DownloadCfg.Youtube.FileExtension = strings.TrimPrefix(cfg.DownloadCfg.Youtube.FileExtension, ".") cfg.ClientCfg.URL = strings.TrimSuffix(cfg.ClientCfg.URL, "/") cfg.NormalizeDir() } +func (cfg *Config) ResolveSystemEnv() { + if cfg.System == "" { + cfg.System = os.Getenv("EXPLO_SYSTEM") + } + if cfg.System == "" { + cfg.System = os.Getenv("MUSIC_SYSTEM_TYPE") + } + cfg.System = strings.ToLower(strings.TrimSpace(cfg.System)) +} + +func (cfg *Config) TrimEnvValues() { + cfg.System = strings.TrimSpace(cfg.System) + cfg.LogLevel = strings.TrimSpace(cfg.LogLevel) + + cfg.ClientCfg.ClientID = strings.TrimSpace(cfg.ClientCfg.ClientID) + cfg.ClientCfg.LibraryName = strings.TrimSpace(cfg.ClientCfg.LibraryName) + cfg.ClientCfg.URL = strings.TrimSpace(cfg.ClientCfg.URL) + cfg.ClientCfg.DownloadDir = strings.TrimSpace(cfg.ClientCfg.DownloadDir) + cfg.ClientCfg.PlaylistDir = strings.TrimSpace(cfg.ClientCfg.PlaylistDir) + cfg.ClientCfg.PlaylistNFormat = strings.TrimSpace(cfg.ClientCfg.PlaylistNFormat) + + cfg.ClientCfg.Creds.APIKey = strings.TrimSpace(cfg.ClientCfg.Creds.APIKey) + cfg.ClientCfg.Creds.User = strings.TrimSpace(cfg.ClientCfg.Creds.User) + cfg.ClientCfg.Creds.Password = strings.TrimSpace(cfg.ClientCfg.Creds.Password) + cfg.ClientCfg.Creds.Listenbrainz = strings.TrimSpace(cfg.ClientCfg.Creds.Listenbrainz) + cfg.ClientCfg.AdminCreds.User = strings.TrimSpace(cfg.ClientCfg.AdminCreds.User) + cfg.ClientCfg.AdminCreds.Password = strings.TrimSpace(cfg.ClientCfg.AdminCreds.Password) + + cfg.ClientCfg.Subsonic.Version = strings.TrimSpace(cfg.ClientCfg.Subsonic.Version) + cfg.ClientCfg.Subsonic.ID = strings.TrimSpace(cfg.ClientCfg.Subsonic.ID) + + cfg.DownloadCfg.DownloadDir = strings.TrimSpace(cfg.DownloadCfg.DownloadDir) + cfg.DownloadCfg.Discovery = strings.TrimSpace(cfg.DownloadCfg.Discovery) + cfg.DownloadCfg.Services = trimStrings(cfg.DownloadCfg.Services) + + cfg.DownloadCfg.Youtube.APIKey = strings.TrimSpace(cfg.DownloadCfg.Youtube.APIKey) + cfg.DownloadCfg.Youtube.FfmpegPath = strings.TrimSpace(cfg.DownloadCfg.Youtube.FfmpegPath) + cfg.DownloadCfg.Youtube.YtdlpPath = strings.TrimSpace(cfg.DownloadCfg.Youtube.YtdlpPath) + cfg.DownloadCfg.Youtube.FileExtension = strings.TrimSpace(cfg.DownloadCfg.Youtube.FileExtension) + cfg.DownloadCfg.Youtube.CookiesPath = strings.TrimSpace(cfg.DownloadCfg.Youtube.CookiesPath) + cfg.DownloadCfg.Youtube.Filters.Extensions = trimStrings(cfg.DownloadCfg.Youtube.Filters.Extensions) + cfg.DownloadCfg.Youtube.Filters.FilterList = trimStrings(cfg.DownloadCfg.Youtube.Filters.FilterList) + + cfg.DownloadCfg.YoutubeMusic.FfmpegPath = strings.TrimSpace(cfg.DownloadCfg.YoutubeMusic.FfmpegPath) + cfg.DownloadCfg.YoutubeMusic.YtdlpPath = strings.TrimSpace(cfg.DownloadCfg.YoutubeMusic.YtdlpPath) + cfg.DownloadCfg.YoutubeMusic.Filters.Extensions = trimStrings(cfg.DownloadCfg.YoutubeMusic.Filters.Extensions) + cfg.DownloadCfg.YoutubeMusic.Filters.FilterList = trimStrings(cfg.DownloadCfg.YoutubeMusic.Filters.FilterList) + + cfg.DownloadCfg.Slskd.APIKey = strings.TrimSpace(cfg.DownloadCfg.Slskd.APIKey) + cfg.DownloadCfg.Slskd.URL = strings.TrimSpace(cfg.DownloadCfg.Slskd.URL) + cfg.DownloadCfg.Slskd.SlskdDir = strings.TrimSpace(cfg.DownloadCfg.Slskd.SlskdDir) + cfg.DownloadCfg.Slskd.Filters.Extensions = trimStrings(cfg.DownloadCfg.Slskd.Filters.Extensions) + cfg.DownloadCfg.Slskd.Filters.FilterList = trimStrings(cfg.DownloadCfg.Slskd.Filters.FilterList) + + cfg.DiscoveryCfg.Discovery = strings.TrimSpace(cfg.DiscoveryCfg.Discovery) + cfg.DiscoveryCfg.Listenbrainz.Discovery = strings.TrimSpace(cfg.DiscoveryCfg.Listenbrainz.Discovery) + cfg.DiscoveryCfg.Listenbrainz.User = strings.TrimSpace(cfg.DiscoveryCfg.Listenbrainz.User) + + cfg.NotifyCfg.Matrix.UserID = strings.TrimSpace(cfg.NotifyCfg.Matrix.UserID) + cfg.NotifyCfg.Matrix.RoomID = strings.TrimSpace(cfg.NotifyCfg.Matrix.RoomID) + cfg.NotifyCfg.Matrix.HomeServer = strings.TrimSpace(cfg.NotifyCfg.Matrix.HomeServer) + cfg.NotifyCfg.Matrix.AccessToken = strings.TrimSpace(cfg.NotifyCfg.Matrix.AccessToken) + cfg.NotifyCfg.Discord.BotToken = strings.TrimSpace(cfg.NotifyCfg.Discord.BotToken) + cfg.NotifyCfg.Discord.ChannelIDs = trimStrings(cfg.NotifyCfg.Discord.ChannelIDs) + cfg.NotifyCfg.Http.ReceiverURLs = trimStrings(cfg.NotifyCfg.Http.ReceiverURLs) +} + +func trimStrings(values []string) []string { + if len(values) == 0 { + return values + } + + trimmed := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + if value == "" { + continue + } + trimmed = append(trimmed, value) + } + + return trimmed +} + func (cfg *Config) NormalizeDir() { if cfg.System == "mpd" { cfg.ClientCfg.PlaylistDir = fixDir(cfg.ClientCfg.PlaylistDir) diff --git a/src/config/config_test.go b/src/config/config_test.go new file mode 100644 index 0000000..9ccfc08 --- /dev/null +++ b/src/config/config_test.go @@ -0,0 +1,268 @@ +package config + +import ( + "testing" +) + +func TestResolveSystemEnv(t *testing.T) { + tests := []struct { + name string + exp string + music string + initial string + wantSystem string + }{ + { + name: "uses EXPLO_SYSTEM when present", + exp: " Plex \n", + music: "emby", + wantSystem: "plex", + }, + { + name: "falls back to MUSIC_SYSTEM_TYPE", + music: " JellyFin\t", + wantSystem: "jellyfin", + }, + { + name: "keeps existing config system if set", + exp: "subsonic", + music: "mpd", + initial: " Emby ", + wantSystem: "emby", + }, + { + name: "empty when none provided", + wantSystem: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv("EXPLO_SYSTEM", tt.exp) + t.Setenv("MUSIC_SYSTEM_TYPE", tt.music) + + cfg := &Config{System: tt.initial} + cfg.ResolveSystemEnv() + + if cfg.System != tt.wantSystem { + t.Fatalf("system mismatch: got %q, want %q", cfg.System, tt.wantSystem) + } + }) + } +} + +func TestTrimEnvValues(t *testing.T) { + cfg := &Config{ + System: " plex \n", + LogLevel: " DEBUG ", + ClientCfg: ClientConfig{ + URL: " https://example.com/ \n", + LibraryName: " Music ", + Creds: Credentials{ + User: " user \n", + Password: " pass \t", + Listenbrainz: " lb_token \n", + }, + }, + DownloadCfg: DownloadConfig{ + DownloadDir: " /data/music \n", + Services: []string{" youtube ", " ", "\nslskd\t"}, + }, + NotifyCfg: NotifyConfig{ + Discord: DiscordNotif{ + ChannelIDs: []string{" 123 ", "", " 456 "}, + }, + Http: HttpNotif{ + ReceiverURLs: []string{" https://a ", " ", "https://b\n"}, + }, + }, + } + + cfg.TrimEnvValues() + + if cfg.System != "plex" { + t.Fatalf("unexpected system after trim: %q", cfg.System) + } + if cfg.LogLevel != "DEBUG" { + t.Fatalf("unexpected log level after trim: %q", cfg.LogLevel) + } + if cfg.ClientCfg.URL != "https://example.com/" { + t.Fatalf("unexpected URL after trim: %q", cfg.ClientCfg.URL) + } + if cfg.ClientCfg.Creds.User != "user" { + t.Fatalf("unexpected user after trim: %q", cfg.ClientCfg.Creds.User) + } + if cfg.ClientCfg.Creds.Password != "pass" { + t.Fatalf("unexpected password after trim: %q", cfg.ClientCfg.Creds.Password) + } + if cfg.ClientCfg.Creds.Listenbrainz != "lb_token" { + t.Fatalf("unexpected listenbrainz token after trim: %q", cfg.ClientCfg.Creds.Listenbrainz) + } + if cfg.DownloadCfg.DownloadDir != "/data/music" { + t.Fatalf("unexpected download dir after trim: %q", cfg.DownloadCfg.DownloadDir) + } + + wantServices := []string{"youtube", "slskd"} + if len(cfg.DownloadCfg.Services) != len(wantServices) { + t.Fatalf("unexpected services length: got %d, want %d", len(cfg.DownloadCfg.Services), len(wantServices)) + } + for i := range wantServices { + if cfg.DownloadCfg.Services[i] != wantServices[i] { + t.Fatalf("unexpected service at index %d: got %q, want %q", i, cfg.DownloadCfg.Services[i], wantServices[i]) + } + } + + wantChannelIDs := []string{"123", "456"} + if len(cfg.NotifyCfg.Discord.ChannelIDs) != len(wantChannelIDs) { + t.Fatalf("unexpected channel IDs length: got %d, want %d", len(cfg.NotifyCfg.Discord.ChannelIDs), len(wantChannelIDs)) + } + for i := range wantChannelIDs { + if cfg.NotifyCfg.Discord.ChannelIDs[i] != wantChannelIDs[i] { + t.Fatalf("unexpected channel id at index %d: got %q, want %q", i, cfg.NotifyCfg.Discord.ChannelIDs[i], wantChannelIDs[i]) + } + } + + wantReceiverURLs := []string{"https://a", "https://b"} + if len(cfg.NotifyCfg.Http.ReceiverURLs) != len(wantReceiverURLs) { + t.Fatalf("unexpected receiver URLs length: got %d, want %d", len(cfg.NotifyCfg.Http.ReceiverURLs), len(wantReceiverURLs)) + } + for i := range wantReceiverURLs { + if cfg.NotifyCfg.Http.ReceiverURLs[i] != wantReceiverURLs[i] { + t.Fatalf("unexpected receiver URL at index %d: got %q, want %q", i, cfg.NotifyCfg.Http.ReceiverURLs[i], wantReceiverURLs[i]) + } + } +} + +func TestCommonFixesNormalizesSystemAndURL(t *testing.T) { + t.Setenv("EXPLO_SYSTEM", "") + t.Setenv("MUSIC_SYSTEM_TYPE", " MPD \n") + + cfg := &Config{ + ClientCfg: ClientConfig{ + URL: " http://localhost:4533/ \n", + PlaylistDir: " /playlists ", + }, + DownloadCfg: DownloadConfig{ + DownloadDir: " /data ", + Slskd: Slskd{ + SlskdDir: " /slskd ", + }, + Youtube: Youtube{ + FileExtension: ".opus", + }, + }, + } + + cfg.CommonFixes() + + if cfg.System != "mpd" { + t.Fatalf("unexpected system after common fixes: %q", cfg.System) + } + if cfg.ClientCfg.URL != "http://localhost:4533" { + t.Fatalf("unexpected URL after common fixes: %q", cfg.ClientCfg.URL) + } + if cfg.ClientCfg.PlaylistDir != "/playlists/" { + t.Fatalf("unexpected playlist dir after common fixes: %q", cfg.ClientCfg.PlaylistDir) + } + if cfg.DownloadCfg.DownloadDir != "/data/" { + t.Fatalf("unexpected download dir after common fixes: %q", cfg.DownloadCfg.DownloadDir) + } + if cfg.DownloadCfg.Slskd.SlskdDir != "/slskd/" { + t.Fatalf("unexpected slskd dir after common fixes: %q", cfg.DownloadCfg.Slskd.SlskdDir) + } + if cfg.DownloadCfg.Youtube.FileExtension != "opus" { + t.Fatalf("unexpected file extension after common fixes: %q", cfg.DownloadCfg.Youtube.FileExtension) + } +} + +func TestApplyManualEnvFallbackMapping(t *testing.T) { + t.Setenv("MUSIC_SYSTEM_TYPE", " plex \n") + t.Setenv("MUSIC_SYSTEM_URL", " https://music.local/ \n") + t.Setenv("MUSIC_SYSTEM_TOKEN", " system_token \n") + t.Setenv("LISTENBRAINZ_USER", " lb_user \n") + t.Setenv("LISTENBRAINZ_TOKEN", " lb_token \n") + t.Setenv("DOWNLOAD_TYPE", " youtube \n") + t.Setenv("DOWNLOAD_DIR", " /data/downloads \n") + + cfg := &Config{} + cfg.applyManualEnvFallback() + + if cfg.System != "plex" { + t.Fatalf("unexpected system from fallback: %q", cfg.System) + } + if cfg.ClientCfg.URL != "https://music.local/" { + t.Fatalf("unexpected url from fallback: %q", cfg.ClientCfg.URL) + } + if cfg.ClientCfg.Creds.APIKey != "system_token" { + t.Fatalf("unexpected api key from fallback: %q", cfg.ClientCfg.Creds.APIKey) + } + if cfg.ClientCfg.Creds.Listenbrainz != "lb_token" { + t.Fatalf("unexpected listenbrainz token from fallback: %q", cfg.ClientCfg.Creds.Listenbrainz) + } + if cfg.DiscoveryCfg.Listenbrainz.User != "lb_user" { + t.Fatalf("unexpected listenbrainz user from fallback: %q", cfg.DiscoveryCfg.Listenbrainz.User) + } + if len(cfg.DownloadCfg.Services) != 1 || cfg.DownloadCfg.Services[0] != "youtube" { + t.Fatalf("unexpected services from fallback: %#v", cfg.DownloadCfg.Services) + } + if cfg.DownloadCfg.DownloadDir != "/data/downloads" { + t.Fatalf("unexpected download dir from fallback: %q", cfg.DownloadCfg.DownloadDir) + } +} + +func TestReadEnvManualLoader(t *testing.T) { + t.Setenv("MUSIC_SYSTEM_TYPE", " subsonic \n") + t.Setenv("MUSIC_SYSTEM_URL", " http://subsonic.local/ \n") + t.Setenv("MUSIC_SYSTEM_TOKEN", " token123 \n") + t.Setenv("LISTENBRAINZ_USER", " lb_user \n") + t.Setenv("LISTENBRAINZ_TOKEN", " lb_token \n") + t.Setenv("DOWNLOAD_TYPE", " slskd \n") + t.Setenv("DOWNLOAD_DIR", " /music/downloads \n") + + cfg := &Config{ + Flags: Flags{ + CfgPath: "__does_not_exist__.env", + }, + } + + cfg.ReadEnv() + + if cfg.System != "subsonic" { + t.Fatalf("unexpected system after manual load: %q", cfg.System) + } + if cfg.ClientCfg.URL != "http://subsonic.local" { + t.Fatalf("unexpected url after manual load: %q", cfg.ClientCfg.URL) + } + if cfg.ClientCfg.Creds.APIKey != "token123" { + t.Fatalf("unexpected api key after manual load: %q", cfg.ClientCfg.Creds.APIKey) + } + if cfg.ClientCfg.Creds.Listenbrainz != "lb_token" { + t.Fatalf("unexpected listenbrainz token after manual load: %q", cfg.ClientCfg.Creds.Listenbrainz) + } + if cfg.DiscoveryCfg.Listenbrainz.User != "lb_user" { + t.Fatalf("unexpected listenbrainz user after manual load: %q", cfg.DiscoveryCfg.Listenbrainz.User) + } + if len(cfg.DownloadCfg.Services) != 1 || cfg.DownloadCfg.Services[0] != "slskd" { + t.Fatalf("unexpected services after manual load: %#v", cfg.DownloadCfg.Services) + } + if cfg.DownloadCfg.DownloadDir != "/music/downloads/" { + t.Fatalf("unexpected download dir after manual load: %q", cfg.DownloadCfg.DownloadDir) + } +} + +func TestReadEnvManualLoaderDefaultsToJellyfin(t *testing.T) { + t.Setenv("MUSIC_SYSTEM_TYPE", "") + t.Setenv("EXPLO_SYSTEM", "") + + cfg := &Config{ + Flags: Flags{ + CfgPath: "__does_not_exist__.env", + }, + } + + cfg.ReadEnv() + + if cfg.System != "jellyfin" { + t.Fatalf("expected default jellyfin system, got %q", cfg.System) + } +} diff --git a/src/discovery/discovery.go b/src/discovery/discovery.go index 9981a2c..d2d2800 100644 --- a/src/discovery/discovery.go +++ b/src/discovery/discovery.go @@ -1,13 +1,14 @@ package discovery import ( + "fmt" "explo/src/models" cfg "explo/src/config" "explo/src/util" ) type DiscoverClient struct { - cfg *cfg.DiscoveryConfig + Config *cfg.DiscoveryConfig Discovery Discovery } type Discovery interface { @@ -15,7 +16,7 @@ type Discovery interface { } func NewDiscoverer(cfg cfg.DiscoveryConfig, httpClient *util.HttpClient) *DiscoverClient { - c := &DiscoverClient{cfg: &cfg} + c := &DiscoverClient{Config: &cfg} switch cfg.Discovery { case "listenbrainz": @@ -27,5 +28,8 @@ func NewDiscoverer(cfg cfg.DiscoveryConfig, httpClient *util.HttpClient) *Discov } func (c *DiscoverClient) Discover() ([]*models.Track, error) { + if c == nil || c.Config == nil || c.Discovery == nil { + return nil, fmt.Errorf("discovery client not initialized") + } return c.Discovery.QueryTracks() } \ No newline at end of file diff --git a/src/main/main.go b/src/main/main.go index e034f4b..2db7293 100644 --- a/src/main/main.go +++ b/src/main/main.go @@ -41,6 +41,7 @@ func main() { cfg.ReadEnv() cfg.MergeFlags() setup(&cfg) + slog.Info("Configuration resolved", "system", cfg.System) slog.Info("Starting Explo...") httpClient := initHttpClient() @@ -49,14 +50,18 @@ func main() { slog.Error(err.Error(), "notify", true) os.Exit(1) } - discovery := discovery.NewDiscoverer(cfg.DiscoveryCfg, httpClient) + discoveryClient := discovery.NewDiscoverer(cfg.DiscoveryCfg, httpClient) + if discoveryClient == nil { + slog.Error("failed to initialize discovery client", "service", cfg.DiscoveryCfg.Discovery, "notify", true) + os.Exit(1) + } downloader, err := downloader.NewDownloader(&cfg.DownloadCfg, httpClient, cfg.Flags.ExcludeLocal) if err != nil { slog.Error(err.Error(), "notify", true) os.Exit(1) } - tracks, err := discovery.Discover() + tracks, err := discoveryClient.Discover() if err != nil { slog.Error(err.Error(), "notify", true) os.Exit(1)