mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
initial Go port of streamrip
This commit is contained in:
331
internal/config/config.go
Normal file
331
internal/config/config.go
Normal file
@@ -0,0 +1,331 @@
|
||||
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"`
|
||||
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"`
|
||||
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"`
|
||||
UseDeezloader bool `toml:"use_deezloader"`
|
||||
DeezloaderWarnings bool `toml:"deezloader_warnings"`
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
var data ConfigData
|
||||
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 (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,
|
||||
},
|
||||
Deezer: DeezerConfig{
|
||||
Quality: 2,
|
||||
LowerQualityIfNotAvailable: true,
|
||||
UseDeezloader: true,
|
||||
DeezloaderWarnings: true,
|
||||
},
|
||||
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
|
||||
}
|
||||
81
internal/config/config_test.go
Normal file
81
internal/config/config_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultConfigData(t *testing.T) {
|
||||
data := DefaultConfigData()
|
||||
if data.Misc.Version != CurrentConfigVersion {
|
||||
t.Fatalf("version = %q, want %q", data.Misc.Version, CurrentConfigVersion)
|
||||
}
|
||||
if data.Downloads.Folder == "" {
|
||||
t.Fatalf("downloads folder should not be empty")
|
||||
}
|
||||
if data.Database.DownloadsPath == "" || data.Database.FailedDownloadsPath == "" {
|
||||
t.Fatalf("database paths should not be empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCreatesDefaultConfigWhenMissing(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "config.toml")
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
if cfg.Path != path {
|
||||
t.Fatalf("path = %q, want %q", cfg.Path, path)
|
||||
}
|
||||
if _, err = os.Stat(path); err != nil {
|
||||
t.Fatalf("expected created config file: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadOutdatedConfig(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "config.toml")
|
||||
|
||||
data := DefaultConfigData()
|
||||
data.Misc.Version = "1.0.0"
|
||||
if err := saveConfigData(path, data); err != nil {
|
||||
t.Fatalf("saveConfigData() error = %v", err)
|
||||
}
|
||||
|
||||
_, err := Load(path)
|
||||
if !errors.Is(err, ErrOutdatedConfig) {
|
||||
t.Fatalf("Load() error = %v, want ErrOutdatedConfig", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSessionCloneDoesNotAliasSlices(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
path := filepath.Join(tmpDir, "config.toml")
|
||||
|
||||
data := DefaultConfigData()
|
||||
data.Metadata.Exclude = []string{"lyrics"}
|
||||
data.Qobuz.Secrets = []string{"s1"}
|
||||
if err := saveConfigData(path, data); err != nil {
|
||||
t.Fatalf("saveConfigData() error = %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load() error = %v", err)
|
||||
}
|
||||
|
||||
cfg.Session.Metadata.Exclude[0] = "comment"
|
||||
cfg.Session.Qobuz.Secrets[0] = "s2"
|
||||
|
||||
if cfg.File.Metadata.Exclude[0] != "lyrics" {
|
||||
t.Fatalf("file metadata exclude unexpectedly mutated")
|
||||
}
|
||||
if cfg.File.Qobuz.Secrets[0] != "s1" {
|
||||
t.Fatalf("file qobuz secrets unexpectedly mutated")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user