feat: add local DB to save downloads
This commit is contained in:
+23
-4
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+5
-54
@@ -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()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package utils
|
||||
|
||||
import "os"
|
||||
|
||||
func FileExists(path string) bool {
|
||||
info, err := os.Stat(path)
|
||||
|
||||
return err == nil && !info.IsDir()
|
||||
}
|
||||
Reference in New Issue
Block a user