package config import ( "errors" "fmt" "os" "path/filepath" "github.com/pelletier/go-toml/v2" ) const CurrentConfigVersion = "2.2.0" var ErrOutdatedConfig = errors.New("config version mismatch") type Config struct { Path string File ConfigData Session ConfigData } type ConfigData struct { Downloads DownloadsConfig `toml:"downloads"` Qobuz QobuzConfig `toml:"qobuz"` Tidal TidalConfig `toml:"tidal"` Deezer DeezerConfig `toml:"deezer"` Yandex YandexConfig `toml:"yandex"` Soundcloud SoundcloudConfig `toml:"soundcloud"` Youtube YoutubeConfig `toml:"youtube"` Database DatabaseConfig `toml:"database"` Conversion ConversionConfig `toml:"conversion"` QobuzFilters QobuzDiscographyFilterConfig `toml:"qobuz_filters"` Artwork ArtworkConfig `toml:"artwork"` Metadata MetadataConfig `toml:"metadata"` Filepaths FilepathsConfig `toml:"filepaths"` LastFM LastFMConfig `toml:"lastfm"` CLI CLIConfig `toml:"cli"` Misc MiscConfig `toml:"misc"` } type DownloadsConfig struct { Folder string `toml:"folder"` SourceSubdirectories bool `toml:"source_subdirectories"` DiscSubdirectories bool `toml:"disc_subdirectories"` Concurrency bool `toml:"concurrency"` MaxConnections int `toml:"max_connections"` RequestsPerMinute int `toml:"requests_per_minute"` VerifySSL bool `toml:"verify_ssl"` } type QobuzConfig struct { Quality int `toml:"quality"` DownloadBooklets bool `toml:"download_booklets"` UseAuthToken bool `toml:"use_auth_token"` EmailOrUserID string `toml:"email_or_userid"` PasswordOrToken string `toml:"password_or_token"` AppID string `toml:"app_id"` Secrets []string `toml:"secrets"` } type TidalConfig struct { Quality int `toml:"quality"` DownloadVideos bool `toml:"download_videos"` PreferAtmos bool `toml:"prefer_atmos"` UserID string `toml:"user_id"` CountryCode string `toml:"country_code"` AccessToken string `toml:"access_token"` RefreshToken string `toml:"refresh_token"` TokenExpiry int64 `toml:"token_expiry"` } type DeezerConfig struct { Quality int `toml:"quality"` LowerQualityIfNotAvailable bool `toml:"lower_quality_if_not_available"` ARL string `toml:"arl"` Email string `toml:"email"` Password string `toml:"password"` RefreshToken string `toml:"refresh_token"` } type YandexConfig struct { Quality int `toml:"quality"` AccessToken string `toml:"access_token"` UserID string `toml:"user_id"` } type SoundcloudConfig struct { Quality int `toml:"quality"` ClientID string `toml:"client_id"` AppVersion string `toml:"app_version"` } type YoutubeConfig struct { Quality int `toml:"quality"` DownloadVideos bool `toml:"download_videos"` VideoDownloadsFolder string `toml:"video_downloads_folder"` } type DatabaseConfig struct { DownloadsEnabled bool `toml:"downloads_enabled"` DownloadsPath string `toml:"downloads_path"` FailedDownloadsEnabled bool `toml:"failed_downloads_enabled"` FailedDownloadsPath string `toml:"failed_downloads_path"` } type ConversionConfig struct { Enabled bool `toml:"enabled"` Codec string `toml:"codec"` SamplingRate int `toml:"sampling_rate"` BitDepth int `toml:"bit_depth"` LossyBitrate int `toml:"lossy_bitrate"` } type QobuzDiscographyFilterConfig struct { Extras bool `toml:"extras"` Repeats bool `toml:"repeats"` NonAlbums bool `toml:"non_albums"` Features bool `toml:"features"` NonStudioAlbums bool `toml:"non_studio_albums"` NonRemaster bool `toml:"non_remaster"` } type ArtworkConfig struct { Embed bool `toml:"embed"` EmbedSize string `toml:"embed_size"` EmbedMaxWidth int `toml:"embed_max_width"` SaveArtwork bool `toml:"save_artwork"` SavedMaxWidth int `toml:"saved_max_width"` } type MetadataConfig struct { SetPlaylistToAlbum bool `toml:"set_playlist_to_album"` RenumberPlaylistTracks bool `toml:"renumber_playlist_tracks"` Exclude []string `toml:"exclude"` } type FilepathsConfig struct { AddSinglesToFolder bool `toml:"add_singles_to_folder"` FolderFormat string `toml:"folder_format"` TrackFormat string `toml:"track_format"` RestrictCharacters bool `toml:"restrict_characters"` TruncateTo int `toml:"truncate_to"` } type LastFMConfig struct { Source string `toml:"source"` FallbackSource string `toml:"fallback_source"` } type CLIConfig struct { TextOutput bool `toml:"text_output"` ProgressBars bool `toml:"progress_bars"` MaxSearchResults int `toml:"max_search_results"` } type MiscConfig struct { Version string `toml:"version"` CheckForUpdates bool `toml:"check_for_updates"` } func Load(path string) (*Config, error) { resolvedPath, err := resolvePath(path) if err != nil { return nil, err } if _, err = os.Stat(resolvedPath); errors.Is(err, os.ErrNotExist) { cfg := DefaultConfigData() if err = saveConfigData(resolvedPath, cfg); err != nil { return nil, err } return &Config{Path: resolvedPath, File: cfg, Session: cloneConfigData(cfg)}, nil } if err != nil { return nil, err } raw, err := os.ReadFile(resolvedPath) if err != nil { return nil, err } data := DefaultConfigData() if err = toml.Unmarshal(raw, &data); err != nil { return nil, err } applyRuntimeDefaults(&data) if data.Misc.Version != CurrentConfigVersion { return nil, fmt.Errorf("%w: need to update from %q to %q", ErrOutdatedConfig, data.Misc.Version, CurrentConfigVersion) } return &Config{Path: resolvedPath, File: data, Session: cloneConfigData(data)}, nil } func UpgradeOutdated(path string) (string, error) { resolvedPath, err := resolvePath(path) if err != nil { return "", err } raw, err := os.ReadFile(resolvedPath) if err != nil { return "", err } data := DefaultConfigData() if err = toml.Unmarshal(raw, &data); err != nil { return "", err } applyRuntimeDefaults(&data) data.Misc.Version = CurrentConfigVersion if err = saveConfigData(resolvedPath, data); err != nil { return "", err } return resolvedPath, nil } func (c *Config) SaveFile() error { return saveConfigData(c.Path, c.File) } func DefaultConfigData() ConfigData { home, _ := os.UserHomeDir() appDir := defaultAppDir() downloadsFolder := filepath.Join(home, "StreamripDownloads") data := ConfigData{ Downloads: DownloadsConfig{ Folder: downloadsFolder, SourceSubdirectories: false, DiscSubdirectories: true, Concurrency: true, MaxConnections: 6, RequestsPerMinute: 60, VerifySSL: true, }, Qobuz: QobuzConfig{ Quality: 3, DownloadBooklets: true, UseAuthToken: false, }, Tidal: TidalConfig{ Quality: 3, DownloadVideos: true, PreferAtmos: false, }, Deezer: DeezerConfig{ Quality: 2, LowerQualityIfNotAvailable: true, }, Yandex: YandexConfig{ Quality: 2, }, Soundcloud: SoundcloudConfig{ Quality: 0, }, Youtube: YoutubeConfig{ Quality: 0, DownloadVideos: false, VideoDownloadsFolder: filepath.Join(downloadsFolder, "YouTubeVideos"), }, Database: DatabaseConfig{ DownloadsEnabled: true, DownloadsPath: filepath.Join(appDir, "downloads.db"), FailedDownloadsEnabled: true, FailedDownloadsPath: filepath.Join(appDir, "failed_downloads.db"), }, Conversion: ConversionConfig{ Enabled: false, Codec: "ALAC", SamplingRate: 48000, BitDepth: 24, LossyBitrate: 320, }, QobuzFilters: QobuzDiscographyFilterConfig{}, Artwork: ArtworkConfig{ Embed: true, EmbedSize: "large", EmbedMaxWidth: -1, SaveArtwork: true, SavedMaxWidth: -1, }, Metadata: MetadataConfig{ SetPlaylistToAlbum: true, RenumberPlaylistTracks: true, Exclude: []string{}, }, Filepaths: FilepathsConfig{ AddSinglesToFolder: false, FolderFormat: "{albumartist} - {title} ({year}) [{container}] [{bit_depth}B-{sampling_rate}kHz]", TrackFormat: "{tracknumber:02}. {artist} - {title}{explicit}", RestrictCharacters: false, TruncateTo: 120, }, LastFM: LastFMConfig{ Source: "qobuz", }, CLI: CLIConfig{ TextOutput: true, ProgressBars: true, MaxSearchResults: 100, }, Misc: MiscConfig{ Version: CurrentConfigVersion, CheckForUpdates: true, }, } return data } func resolvePath(path string) (string, error) { if path != "" { return path, os.MkdirAll(filepath.Dir(path), 0o755) } appDir := defaultAppDir() if err := os.MkdirAll(appDir, 0o755); err != nil { return "", err } return filepath.Join(appDir, "config.toml"), nil } func defaultAppDir() string { base, err := os.UserConfigDir() if err != nil { return "." } return filepath.Join(base, "streamrip") } func saveConfigData(path string, data ConfigData) error { if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { return err } b, err := toml.Marshal(data) if err != nil { return err } return os.WriteFile(path, b, 0o644) } func applyRuntimeDefaults(data *ConfigData) { home, _ := os.UserHomeDir() appDir := defaultAppDir() if data.Downloads.Folder == "" { data.Downloads.Folder = filepath.Join(home, "StreamripDownloads") } if data.Database.DownloadsPath == "" { data.Database.DownloadsPath = filepath.Join(appDir, "downloads.db") } if data.Database.FailedDownloadsPath == "" { data.Database.FailedDownloadsPath = filepath.Join(appDir, "failed_downloads.db") } if data.Youtube.VideoDownloadsFolder == "" { data.Youtube.VideoDownloadsFolder = filepath.Join(data.Downloads.Folder, "YouTubeVideos") } } func cloneConfigData(in ConfigData) ConfigData { out := in out.Qobuz.Secrets = append([]string(nil), in.Qobuz.Secrets...) out.Metadata.Exclude = append([]string(nil), in.Metadata.Exclude...) return out }