diff --git a/.example-config b/.example-config index a81e41e..c4a2db5 100644 --- a/.example-config +++ b/.example-config @@ -1,2 +1,3 @@ arl_cookie = '1d9e90abb452a61b1b7463f0953e1b303c4e2e7e5fd404fde9b385f4de01c340ac0f62f1c8c1550405b0b9beded0e28c481e96a6148e8f4548351add5d7db746b2785ecf83b1768e5dd8cc73b1ad30c18d07c9cb37f5c6b9cd7a78a4de2aff11' secret_key = 'hTv1IAw19qWy9i3f' +output_dir = '' diff --git a/cmd/download.go b/cmd/download.go index ce97f77..7d2dc64 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -20,7 +20,6 @@ var downloadCmd = &cobra.Command{ func init() { RootCmd.AddCommand(downloadCmd) - downloadCmd.PersistentFlags().StringVarP(&opts.OutputDir, "output", "o", "", "output directory (default $HOME/Music/GoDeez)") downloadCmd.PersistentFlags().StringVarP(&opts.Quality, "quality", "q", "best", "download quality [mp3_128, mp3_320, flac, best]") downloadCmd.PersistentFlags().DurationVarP(&opts.Timeout, "timeout", "t", 2*time.Minute, "timeout for each download (e.g. 10s, 1m, 2m30s)") downloadCmd.PersistentFlags().BoolVar(&opts.BPM, "bpm", false, "fetch BPM/key and add to file tags") @@ -37,11 +36,11 @@ func newDownloadCmd(resourceType string) *cobra.Command { Short: fmt.Sprintf("Download songs from %s", resourceType), Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { - return opts.Validate(appCtx.AppDir) + return opts.Validate() }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - dl := downloader.New(appCtx, resourceType) + dl := downloader.New(appConfig, resourceType) if err := dl.Run(ctx, opts, args[0]); err != nil { if errors.Is(err, context.Canceled) { diff --git a/cmd/root.go b/cmd/root.go index b4653b2..a2441e5 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,13 +1,13 @@ package cmd import ( - "github.com/mathismqn/godeez/internal/app" + "github.com/mathismqn/godeez/internal/config" "github.com/spf13/cobra" ) var ( - cfgPath string - appCtx *app.Context + cfgPath string + appConfig *config.Config ) var RootCmd = &cobra.Command{ @@ -16,7 +16,7 @@ var RootCmd = &cobra.Command{ SilenceUsage: true, PersistentPreRunE: func(cmd *cobra.Command, args []string) error { var err error - appCtx, err = app.NewContext(cfgPath) + appConfig, err = config.New(cfgPath) return err }, diff --git a/internal/app/context.go b/internal/app/context.go deleted file mode 100644 index ba519c1..0000000 --- a/internal/app/context.go +++ /dev/null @@ -1,53 +0,0 @@ -package app - -import ( - "fmt" - "os" - "path" - "path/filepath" - - "github.com/mathismqn/godeez/internal/config" - "github.com/mathismqn/godeez/internal/fileutil" - "github.com/mathismqn/godeez/internal/store" -) - -type Context struct { - AppDir string - Config *config.Config -} - -func NewContext(cfgPath string) (*Context, error) { - home, err := os.UserHomeDir() - if err != nil { - return nil, fmt.Errorf("failed to get home directory: %w", err) - } - - cfgDir := filepath.Join(home, ".godeez") - if err := fileutil.EnsureDir(cfgDir); err != nil { - return nil, fmt.Errorf("failed to create config directory: %w", err) - } - - musicDir := filepath.Join(home, "Music") - if err := fileutil.EnsureDir(musicDir); err != nil { - return nil, fmt.Errorf("failed to create music directory: %w", err) - } - - appDir := path.Join(musicDir, "GoDeez") - if err := fileutil.EnsureDir(appDir); err != nil { - return nil, fmt.Errorf("failed to create app directory: %w", err) - } - - cfg, err := config.New(cfgPath, cfgDir) - if err != nil { - return nil, err - } - - if err := store.OpenDB(cfgDir); err != nil { - return nil, err - } - - return &Context{ - AppDir: appDir, - Config: cfg, - }, nil -} diff --git a/internal/config/config.go b/internal/config/config.go index 0aa813d..72e09b4 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -4,53 +4,79 @@ import ( "fmt" "os" "path" + "path/filepath" + "github.com/mathismqn/godeez/internal/fileutil" + "github.com/mathismqn/godeez/internal/store" "github.com/spf13/viper" ) type Config struct { ArlCookie string `mapstructure:"arl_cookie"` SecretKey string `mapstructure:"secret_key"` + OutputDir string `mapstructure:"output_dir"` } -func New(cfgPath, cfgDir string) (*Config, error) { - if cfgPath != "" { - viper.SetConfigFile(cfgPath) - } else { - cfgPath := path.Join(cfgDir, "config.toml") +func New(cfgPath string) (*Config, error) { + homeDir, err := os.UserHomeDir() + if err != nil { + return nil, fmt.Errorf("failed to get home directory: %w", err) + } + + cfgDir := filepath.Join(homeDir, ".godeez") + if err := fileutil.EnsureDir(cfgDir); err != nil { + return nil, fmt.Errorf("failed to create config directory: %w", err) + } + + if cfgPath == "" { + cfgPath = path.Join(cfgDir, "config.toml") if _, err := os.Stat(cfgPath); os.IsNotExist(err) { fmt.Printf("Config file not found, creating one at %s\n", cfgPath) - content := []byte("arl_cookie = ''\nsecret_key = ''\n") + content := []byte("arl_cookie = ''\nsecret_key = ''\noutput_dir = ''\n") if err := os.WriteFile(cfgPath, content, 0644); err != nil { return nil, fmt.Errorf("failed to create config file: %w", err) } - } - viper.AddConfigPath(cfgDir) - viper.SetConfigName("config.toml") + os.Exit(0) + } } + viper.SetConfigFile(cfgPath) viper.SetConfigType("toml") viper.AutomaticEnv() if err := viper.ReadInConfig(); err != nil { return nil, fmt.Errorf("failed to read config file: %w", err) } - cfg := &Config{} + var cfg Config if err := viper.Unmarshal(&cfg); err != nil { - return nil, fmt.Errorf("failed to unmarshal config: %w", err) + return nil, fmt.Errorf("failed to parse config: %w", err) + } + if err := cfg.Validate(homeDir); err != nil { + return nil, fmt.Errorf("invalid config: %w", err) } - if cfg.ArlCookie == "" { - return nil, fmt.Errorf("arl_cookie is not set in config file") - } - if cfg.SecretKey == "" { - return nil, fmt.Errorf("secret_key is not set in config file") - } - if len(cfg.SecretKey) != 16 { - return nil, fmt.Errorf("secret_key must be 16 bytes long") + if err := store.OpenDB(cfgDir); err != nil { + return nil, err } - return cfg, nil + return &cfg, nil +} + +func (c *Config) Validate(homeDir string) error { + if c.ArlCookie == "" { + return fmt.Errorf("arl_cookie is not set") + } + if c.SecretKey == "" { + return fmt.Errorf("secret_key is not set") + } + if len(c.SecretKey) != 16 { + return fmt.Errorf("secret_key must be 16 bytes long") + } + if c.OutputDir == "" { + c.OutputDir = filepath.Join(homeDir, "Music", "GoDeez") + } + + return nil } diff --git a/internal/deezer/client.go b/internal/deezer/client.go index 8a1059f..9461780 100644 --- a/internal/deezer/client.go +++ b/internal/deezer/client.go @@ -9,23 +9,23 @@ import ( "net/http" "strings" - "github.com/mathismqn/godeez/internal/app" + "github.com/mathismqn/godeez/internal/config" ) type Client struct { - AppCtx *app.Context - Session *Session + AppConfig *config.Config + Session *Session } -func NewClient(ctx context.Context, appCtx *app.Context) (*Client, error) { - session, err := Authenticate(ctx, appCtx.Config.ArlCookie) +func NewClient(ctx context.Context, appConfig *config.Config) (*Client, error) { + session, err := Authenticate(ctx, appConfig.ArlCookie) if err != nil { return nil, fmt.Errorf("failed to authenticate: %w", err) } return &Client{ - AppCtx: appCtx, - Session: session, + AppConfig: appConfig, + Session: session, }, nil } diff --git a/internal/downloader/client.go b/internal/downloader/client.go index b023dab..cedef63 100644 --- a/internal/downloader/client.go +++ b/internal/downloader/client.go @@ -11,8 +11,8 @@ import ( "time" "github.com/briandowns/spinner" - "github.com/mathismqn/godeez/internal/app" "github.com/mathismqn/godeez/internal/bpm" + "github.com/mathismqn/godeez/internal/config" "github.com/mathismqn/godeez/internal/crypto" "github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/fileutil" @@ -23,7 +23,7 @@ import ( const chunkSize = 2048 type Client struct { - appCtx *app.Context + appConfig *config.Config resourceType string deezerClient *deezer.Client @@ -32,9 +32,9 @@ type Client struct { hashIndexErr error } -func New(appCtx *app.Context, resourceType string) *Client { +func New(appConfig *config.Config, resourceType string) *Client { return &Client{ - appCtx: appCtx, + appConfig: appConfig, resourceType: resourceType, deezerClient: nil, } @@ -42,7 +42,7 @@ func New(appCtx *app.Context, resourceType string) *Client { func (c *Client) Run(ctx context.Context, opts Options, id string) error { var err error - c.deezerClient, err = deezer.NewClient(ctx, c.appCtx) + c.deezerClient, err = deezer.NewClient(ctx, c.appConfig) if err != nil { return err } @@ -66,8 +66,9 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error { return fmt.Errorf("%s has no songs", c.resourceType) } - outputDir := resource.GetOutputDir(opts.OutputDir) - if err := fileutil.EnsureDir(outputDir); err != nil { + rootOutputDir := c.appConfig.OutputDir + resourceOutputDir := resource.GetOutputDir(rootOutputDir) + if err := fileutil.EnsureDir(resourceOutputDir); err != nil { return fmt.Errorf("failed to create output directory: %w", err) } @@ -91,7 +92,7 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error { sp.Suffix = fmt.Sprintf(" Downloading: %s - %s", song.Artist, song.Title) sp.Start() - warnings, err := c.downloadSong(ctx, resource, song, opts, outputDir) + warnings, err := c.downloadSong(ctx, resource, song, opts, resourceOutputDir) sp.Stop() if err != nil { @@ -135,7 +136,7 @@ Files saved to: %s skipped, failed, time.Since(startTime).Round(time.Second), - outputDir, + resourceOutputDir, ) return nil @@ -185,7 +186,7 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() - key := crypto.GetKey(c.appCtx.Config.SecretKey, song.ID) + key := crypto.GetKey(c.appConfig.SecretKey, song.ID) if err := c.streamToFile(dlCtx, stream, outputPath, key); err != nil { fileutil.DeleteFile(outputPath) @@ -299,7 +300,7 @@ func (c *Client) finalizeDownload(resource deezer.Resource, song *deezer.Song, o func (c *Client) initHashIndex(ctx context.Context) error { c.hashIndexOnce.Do(func() { - c.hashIndex, c.hashIndexErr = fileutil.NewHashIndex(ctx, c.appCtx.AppDir) + c.hashIndex, c.hashIndexErr = fileutil.NewHashIndex(ctx, c.appConfig.OutputDir) }) return c.hashIndexErr diff --git a/internal/downloader/options.go b/internal/downloader/options.go index 0c9227d..eb7a53e 100644 --- a/internal/downloader/options.go +++ b/internal/downloader/options.go @@ -13,21 +13,12 @@ var validQualities = map[string]bool{ } type Options struct { - OutputDir string - Quality string - Timeout time.Duration - BPM bool + Quality string + Timeout time.Duration + BPM bool } -func (o *Options) Validate(appDir string) error { - if o.OutputDir == "" { - o.OutputDir = appDir - } - - if o.Quality == "" { - o.Quality = "best" - } - +func (o *Options) Validate() error { if !validQualities[o.Quality] { return fmt.Errorf("invalid quality option: %s", o.Quality) }