refactor: rework output directory handling
This commit is contained in:
@@ -1,2 +1,3 @@
|
|||||||
arl_cookie = '1d9e90abb452a61b1b7463f0953e1b303c4e2e7e5fd404fde9b385f4de01c340ac0f62f1c8c1550405b0b9beded0e28c481e96a6148e8f4548351add5d7db746b2785ecf83b1768e5dd8cc73b1ad30c18d07c9cb37f5c6b9cd7a78a4de2aff11'
|
arl_cookie = '1d9e90abb452a61b1b7463f0953e1b303c4e2e7e5fd404fde9b385f4de01c340ac0f62f1c8c1550405b0b9beded0e28c481e96a6148e8f4548351add5d7db746b2785ecf83b1768e5dd8cc73b1ad30c18d07c9cb37f5c6b9cd7a78a4de2aff11'
|
||||||
secret_key = 'hTv1IAw19qWy9i3f'
|
secret_key = 'hTv1IAw19qWy9i3f'
|
||||||
|
output_dir = ''
|
||||||
|
|||||||
+2
-3
@@ -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
@@ -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
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user