refactor: rework output directory handling

This commit is contained in:
Mathis Maquenne
2025-05-19 15:38:18 +02:00
parent 674eb99ffd
commit 68f22ce571
8 changed files with 76 additions and 111 deletions
+1
View File
@@ -1,2 +1,3 @@
arl_cookie = '1d9e90abb452a61b1b7463f0953e1b303c4e2e7e5fd404fde9b385f4de01c340ac0f62f1c8c1550405b0b9beded0e28c481e96a6148e8f4548351add5d7db746b2785ecf83b1768e5dd8cc73b1ad30c18d07c9cb37f5c6b9cd7a78a4de2aff11' arl_cookie = '1d9e90abb452a61b1b7463f0953e1b303c4e2e7e5fd404fde9b385f4de01c340ac0f62f1c8c1550405b0b9beded0e28c481e96a6148e8f4548351add5d7db746b2785ecf83b1768e5dd8cc73b1ad30c18d07c9cb37f5c6b9cd7a78a4de2aff11'
secret_key = 'hTv1IAw19qWy9i3f' secret_key = 'hTv1IAw19qWy9i3f'
output_dir = ''
+2 -3
View File
@@ -20,7 +20,6 @@ var downloadCmd = &cobra.Command{
func init() { func init() {
RootCmd.AddCommand(downloadCmd) 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().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().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") 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), Short: fmt.Sprintf("Download songs from %s", resourceType),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
return opts.Validate(appCtx.AppDir) return opts.Validate()
}, },
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
ctx := cmd.Context() ctx := cmd.Context()
dl := downloader.New(appCtx, resourceType) dl := downloader.New(appConfig, resourceType)
if err := dl.Run(ctx, opts, args[0]); err != nil { if err := dl.Run(ctx, opts, args[0]); err != nil {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
+4 -4
View File
@@ -1,13 +1,13 @@
package cmd package cmd
import ( import (
"github.com/mathismqn/godeez/internal/app" "github.com/mathismqn/godeez/internal/config"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var ( var (
cfgPath string cfgPath string
appCtx *app.Context appConfig *config.Config
) )
var RootCmd = &cobra.Command{ var RootCmd = &cobra.Command{
@@ -16,7 +16,7 @@ var RootCmd = &cobra.Command{
SilenceUsage: true, SilenceUsage: true,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error { PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
var err error var err error
appCtx, err = app.NewContext(cfgPath) appConfig, err = config.New(cfgPath)
return err return err
}, },
-53
View File
@@ -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
}
+46 -20
View File
@@ -4,53 +4,79 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"path/filepath"
"github.com/mathismqn/godeez/internal/fileutil"
"github.com/mathismqn/godeez/internal/store"
"github.com/spf13/viper" "github.com/spf13/viper"
) )
type Config struct { type Config struct {
ArlCookie string `mapstructure:"arl_cookie"` ArlCookie string `mapstructure:"arl_cookie"`
SecretKey string `mapstructure:"secret_key"` SecretKey string `mapstructure:"secret_key"`
OutputDir string `mapstructure:"output_dir"`
} }
func New(cfgPath, cfgDir string) (*Config, error) { func New(cfgPath string) (*Config, error) {
if cfgPath != "" { homeDir, err := os.UserHomeDir()
viper.SetConfigFile(cfgPath) if err != nil {
} else { return nil, fmt.Errorf("failed to get home directory: %w", err)
cfgPath := path.Join(cfgDir, "config.toml") }
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) { if _, err := os.Stat(cfgPath); os.IsNotExist(err) {
fmt.Printf("Config file not found, creating one at %s\n", cfgPath) 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 { if err := os.WriteFile(cfgPath, content, 0644); err != nil {
return nil, fmt.Errorf("failed to create config file: %w", err) return nil, fmt.Errorf("failed to create config file: %w", err)
} }
}
viper.AddConfigPath(cfgDir) os.Exit(0)
viper.SetConfigName("config.toml") }
} }
viper.SetConfigFile(cfgPath)
viper.SetConfigType("toml") viper.SetConfigType("toml")
viper.AutomaticEnv() viper.AutomaticEnv()
if err := viper.ReadInConfig(); err != nil { if err := viper.ReadInConfig(); err != nil {
return nil, fmt.Errorf("failed to read config file: %w", err) return nil, fmt.Errorf("failed to read config file: %w", err)
} }
cfg := &Config{} var cfg Config
if err := viper.Unmarshal(&cfg); err != nil { 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 == "" { if err := store.OpenDB(cfgDir); err != nil {
return nil, fmt.Errorf("arl_cookie is not set in config file") return nil, err
}
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")
} }
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
} }
+7 -7
View File
@@ -9,23 +9,23 @@ import (
"net/http" "net/http"
"strings" "strings"
"github.com/mathismqn/godeez/internal/app" "github.com/mathismqn/godeez/internal/config"
) )
type Client struct { type Client struct {
AppCtx *app.Context AppConfig *config.Config
Session *Session Session *Session
} }
func NewClient(ctx context.Context, appCtx *app.Context) (*Client, error) { func NewClient(ctx context.Context, appConfig *config.Config) (*Client, error) {
session, err := Authenticate(ctx, appCtx.Config.ArlCookie) session, err := Authenticate(ctx, appConfig.ArlCookie)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to authenticate: %w", err) return nil, fmt.Errorf("failed to authenticate: %w", err)
} }
return &Client{ return &Client{
AppCtx: appCtx, AppConfig: appConfig,
Session: session, Session: session,
}, nil }, nil
} }
+12 -11
View File
@@ -11,8 +11,8 @@ import (
"time" "time"
"github.com/briandowns/spinner" "github.com/briandowns/spinner"
"github.com/mathismqn/godeez/internal/app"
"github.com/mathismqn/godeez/internal/bpm" "github.com/mathismqn/godeez/internal/bpm"
"github.com/mathismqn/godeez/internal/config"
"github.com/mathismqn/godeez/internal/crypto" "github.com/mathismqn/godeez/internal/crypto"
"github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/deezer"
"github.com/mathismqn/godeez/internal/fileutil" "github.com/mathismqn/godeez/internal/fileutil"
@@ -23,7 +23,7 @@ import (
const chunkSize = 2048 const chunkSize = 2048
type Client struct { type Client struct {
appCtx *app.Context appConfig *config.Config
resourceType string resourceType string
deezerClient *deezer.Client deezerClient *deezer.Client
@@ -32,9 +32,9 @@ type Client struct {
hashIndexErr error hashIndexErr error
} }
func New(appCtx *app.Context, resourceType string) *Client { func New(appConfig *config.Config, resourceType string) *Client {
return &Client{ return &Client{
appCtx: appCtx, appConfig: appConfig,
resourceType: resourceType, resourceType: resourceType,
deezerClient: nil, 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 { func (c *Client) Run(ctx context.Context, opts Options, id string) error {
var err error var err error
c.deezerClient, err = deezer.NewClient(ctx, c.appCtx) c.deezerClient, err = deezer.NewClient(ctx, c.appConfig)
if err != nil { if err != nil {
return err 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) return fmt.Errorf("%s has no songs", c.resourceType)
} }
outputDir := resource.GetOutputDir(opts.OutputDir) rootOutputDir := c.appConfig.OutputDir
if err := fileutil.EnsureDir(outputDir); err != nil { resourceOutputDir := resource.GetOutputDir(rootOutputDir)
if err := fileutil.EnsureDir(resourceOutputDir); err != nil {
return fmt.Errorf("failed to create output directory: %w", err) 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.Suffix = fmt.Sprintf(" Downloading: %s - %s", song.Artist, song.Title)
sp.Start() sp.Start()
warnings, err := c.downloadSong(ctx, resource, song, opts, outputDir) warnings, err := c.downloadSong(ctx, resource, song, opts, resourceOutputDir)
sp.Stop() sp.Stop()
if err != nil { if err != nil {
@@ -135,7 +136,7 @@ Files saved to: %s
skipped, skipped,
failed, failed,
time.Since(startTime).Round(time.Second), time.Since(startTime).Round(time.Second),
outputDir, resourceOutputDir,
) )
return nil return nil
@@ -185,7 +186,7 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son
dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout) dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel() 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 { if err := c.streamToFile(dlCtx, stream, outputPath, key); err != nil {
fileutil.DeleteFile(outputPath) 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 { func (c *Client) initHashIndex(ctx context.Context) error {
c.hashIndexOnce.Do(func() { 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 return c.hashIndexErr
+4 -13
View File
@@ -13,21 +13,12 @@ var validQualities = map[string]bool{
} }
type Options struct { type Options struct {
OutputDir string Quality string
Quality string Timeout time.Duration
Timeout time.Duration BPM bool
BPM bool
} }
func (o *Options) Validate(appDir string) error { func (o *Options) Validate() error {
if o.OutputDir == "" {
o.OutputDir = appDir
}
if o.Quality == "" {
o.Quality = "best"
}
if !validQualities[o.Quality] { if !validQualities[o.Quality] {
return fmt.Errorf("invalid quality option: %s", o.Quality) return fmt.Errorf("invalid quality option: %s", o.Quality)
} }