mirror of
https://git.sr.ht/~joren/streamrip-go
synced 2026-06-17 15:05:39 +02:00
354 lines
10 KiB
Go
354 lines
10 KiB
Go
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"`
|
|
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 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,
|
|
},
|
|
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
|
|
}
|