diff --git a/cmd/download.go b/cmd/download.go index bbc1053..376b8aa 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -5,9 +5,11 @@ import ( "os" "path" "strings" + "time" "github.com/flytam/filenamify" "github.com/mathismqn/godeez/internal/config" + "github.com/mathismqn/godeez/internal/db" "github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/tags" "github.com/mathismqn/godeez/internal/utils" @@ -126,11 +128,9 @@ func downloadContent(contentType string, args []string) { songTitle = fmt.Sprintf("%s %s", song.Title, song.Version) } - fmt.Printf(" Downloading %s...", songTitle) - media, err := song.GetMediaData(session.LicenseToken, quality) if err != nil { - fmt.Printf("\r Downloading %s... FAILED\n", songTitle) + fmt.Printf(" Downloading %s... FAILED\n", songTitle) fmt.Fprintf(os.Stderr, "Error: could not get media data: %v\n", err) if err.Error() == "invalid license token" { os.Exit(1) @@ -139,7 +139,7 @@ func downloadContent(contentType string, args []string) { } if len(media.Data) == 0 || len(media.Data[0].Media) == 0 || len(media.Data[0].Media[0].Sources) == 0 { - fmt.Printf("\r Downloading %s... FAILED\n", songTitle) + fmt.Printf(" Downloading %s... FAILED\n", songTitle) fmt.Fprintf(os.Stderr, "Error: could not get media sources\n") continue } @@ -165,6 +165,15 @@ func downloadContent(contentType string, args []string) { fileName, _ = filenamify.Filenamify(fileName, filenamify.Options{}) filePath := path.Join(output, fileName) + if existing, err := db.Get(song.ID); err == nil { + if existing.Quality == media.Data[0].Media[0].Format && utils.FileExists(existing.Path) { + fmt.Printf(" Skipping %s (already downloaded at %s)\n", songTitle, existing.Path) + continue + } + } + + fmt.Printf(" Downloading %s...", songTitle) + err = media.Download(url, filePath, song.ID) if err != nil { fmt.Printf("\r Downloading %s... FAILED\n", songTitle) @@ -184,6 +193,16 @@ func downloadContent(contentType string, args []string) { if err := tags.AddTags(resource, song, filePath, tempo, key); err != nil { fmt.Fprintf(os.Stderr, "Warning: could not add tags to song: %v\n", err) } + + info := &db.DownloadInfo{ + SongID: song.ID, + Quality: media.Data[0].Media[0].Format, + Path: filePath, + Downloaded: time.Now(), + } + if err := info.Save(); err != nil { + fmt.Fprintf(os.Stderr, "Warning: could not save download info: %v\n", err) + } } } diff --git a/cmd/root.go b/cmd/root.go index 65afc34..077a854 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,13 +1,9 @@ package cmd import ( - "fmt" - "os" - "path" - "github.com/mathismqn/godeez/internal/config" + "github.com/mathismqn/godeez/internal/db" "github.com/spf13/cobra" - "github.com/spf13/viper" ) var cfgFile string @@ -22,53 +18,8 @@ var RootCmd = &cobra.Command{ func init() { RootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.godeez)") - cobra.OnInitialize(initConfig) -} - -func initConfig() { - if cfgFile != "" { - viper.SetConfigFile(cfgFile) - } else { - homedir, err := os.UserHomeDir() - if err != nil { - fmt.Fprintf(os.Stderr, "Error: could not get home directory: %v\n", err) - os.Exit(1) - } - - path := path.Join(homedir, ".godeez") - if _, err := os.Stat(path); os.IsNotExist(err) { - fmt.Printf("Config file not found, creating one at %s\n", path) - - content := []byte("arl_cookie = ''\nsecret_key = ''\niv = '0001020304050607'\n") - if err := os.WriteFile(path, content, 0644); err != nil { - fmt.Fprintf(os.Stderr, "Error: could not create config file: %v\n", err) - os.Exit(1) - } - } - - viper.AddConfigPath(homedir) - viper.SetConfigName(".godeez") - } - - viper.SetConfigType("toml") - viper.AutomaticEnv() - if err := viper.ReadInConfig(); err != nil { - fmt.Fprintf(os.Stderr, "Error: could not read config file: %v\n", err) - os.Exit(1) - } - - cfg := &config.Cfg - if err := viper.Unmarshal(&cfg); err != nil { - fmt.Fprintf(os.Stderr, "Error: could not unmarshal config file: %v\n", err) - os.Exit(1) - } - - if cfg.SecretKey == "" { - fmt.Fprintln(os.Stderr, "Error: secret_key is not set in config file") - os.Exit(1) - } - if cfg.IV == "" { - fmt.Fprintln(os.Stderr, "Error: iv is not set in config file") - os.Exit(1) - } + cobra.OnInitialize(func() { + config.Init(cfgFile) + db.Init() + }) } diff --git a/go.mod b/go.mod index f505774..ff120d0 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.29.0 // indirect - golang.org/x/sys v0.26.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/text v0.19.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect @@ -38,6 +38,7 @@ require ( github.com/go-flac/flacvorbis/v2 v2.0.2 github.com/go-flac/go-flac/v2 v2.0.1 github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.6 // indirect + go.etcd.io/bbolt v1.4.0 golang.org/x/crypto v0.28.0 ) diff --git a/go.sum b/go.sum index fbf9919..c8d9eab 100644 --- a/go.sum +++ b/go.sum @@ -55,8 +55,9 @@ github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= @@ -67,11 +68,14 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.etcd.io/bbolt v1.4.0 h1:TU77id3TnN/zKr7CO/uk+fBCwF2jGcMuw2B/FMAzYIk= +go.etcd.io/bbolt v1.4.0/go.mod h1:AsD+OCi/qPN1giOX1aiLAha3o1U8rAz65bvN4j0sRuk= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= @@ -94,6 +98,8 @@ golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -101,8 +107,8 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= -golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/internal/config/config.go b/internal/config/config.go index fa54efe..8412315 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -1,5 +1,14 @@ package config +import ( + "fmt" + "os" + "path" + + "github.com/mathismqn/godeez/internal/utils" + "github.com/spf13/viper" +) + type Config struct { ArlCookie string `mapstructure:"arl_cookie"` SecretKey string `mapstructure:"secret_key"` @@ -7,3 +16,57 @@ type Config struct { } var Cfg Config + +func Init(cfgFile string) { + if cfgFile != "" { + viper.SetConfigFile(cfgFile) + } else { + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: could not get home directory: %v\n", err) + os.Exit(1) + } + + appDir := path.Join(homeDir, ".godeez") + if err := utils.EnsureDir(appDir); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not create app directory: %v\n", err) + os.Exit(1) + } + + cfgPath := path.Join(appDir, "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 = ''\niv = '0001020304050607'\n") + if err := os.WriteFile(cfgPath, content, 0644); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not create config file: %v\n", err) + os.Exit(1) + } + } + + viper.AddConfigPath(appDir) + viper.SetConfigName("config.toml") + } + + viper.SetConfigType("toml") + viper.AutomaticEnv() + if err := viper.ReadInConfig(); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not read config file: %v\n", err) + os.Exit(1) + } + + cfg := &Cfg + if err := viper.Unmarshal(&cfg); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not unmarshal config file: %v\n", err) + os.Exit(1) + } + + if cfg.SecretKey == "" { + fmt.Fprintln(os.Stderr, "Error: secret_key is not set in config file") + os.Exit(1) + } + if cfg.IV == "" { + fmt.Fprintln(os.Stderr, "Error: iv is not set in config file") + os.Exit(1) + } +} diff --git a/internal/db/download_info.go b/internal/db/download_info.go new file mode 100644 index 0000000..1a16ce0 --- /dev/null +++ b/internal/db/download_info.go @@ -0,0 +1,58 @@ +package db + +import ( + "encoding/json" + "fmt" + "time" + + "go.etcd.io/bbolt" +) + +type DownloadInfo struct { + SongID string `json:"song_id"` + Quality string `json:"quality"` + Path string `json:"path"` + Downloaded time.Time `json:"downloaded_at"` +} + +func Get(songID string) (*DownloadInfo, error) { + var info DownloadInfo + + if err := db.View(func(tx *bbolt.Tx) error { + b := tx.Bucket(trackBucket) + if b == nil { + return fmt.Errorf("bucket not found") + } + + data := b.Get([]byte(songID)) + if data == nil { + return fmt.Errorf("not found") + } + return json.Unmarshal(data, &info) + }); err != nil { + return nil, err + } + + return &info, nil +} + +func (d *DownloadInfo) Save() error { + return db.Update(func(tx *bbolt.Tx) error { + b := tx.Bucket(trackBucket) + if b == nil { + return fmt.Errorf("bucket not found") + } + + data, err := json.Marshal(d) + if err != nil { + return err + } + + err = b.Put([]byte(d.SongID), data) + if err != nil { + return err + } + + return nil + }) +} diff --git a/internal/db/setup.go b/internal/db/setup.go new file mode 100644 index 0000000..8ac564f --- /dev/null +++ b/internal/db/setup.go @@ -0,0 +1,44 @@ +package db + +import ( + "fmt" + "os" + "path" + + "github.com/mathismqn/godeez/internal/utils" + bolt "go.etcd.io/bbolt" +) + +var ( + db *bolt.DB + trackBucket = []byte("tracks") +) + +func Init() { + homeDir, err := os.UserHomeDir() + if err != nil { + fmt.Fprintf(os.Stderr, "Error: could not get home directory: %v\n", err) + os.Exit(1) + } + + appDir := path.Join(homeDir, ".godeez") + if err := utils.EnsureDir(appDir); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not create app directory: %v\n", err) + os.Exit(1) + } + + dbPath := path.Join(appDir, "tracks.db") + db, err = bolt.Open(dbPath, 0600, nil) + if err != nil { + fmt.Fprintf(os.Stderr, "Error: could not open database: %v\n", err) + os.Exit(1) + } + + if err := db.Update(func(tx *bolt.Tx) error { + _, err := tx.CreateBucketIfNotExists(trackBucket) + return err + }); err != nil { + fmt.Fprintf(os.Stderr, "Error: could not create bucket: %v\n", err) + os.Exit(1) + } +} diff --git a/internal/utils/file.go b/internal/utils/file.go new file mode 100644 index 0000000..eb899ee --- /dev/null +++ b/internal/utils/file.go @@ -0,0 +1,9 @@ +package utils + +import "os" + +func FileExists(path string) bool { + info, err := os.Stat(path) + + return err == nil && !info.IsDir() +}