Merge branch 'feature/download-track' into dev
This commit is contained in:
@@ -19,7 +19,7 @@ A simple Go tool for downloading music from [Deezer](https://www.deezer.com).
|
|||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Download playlists, albums, and artists' top tracks from Deezer
|
* Download playlists, albums, artists' top tracks, and individual tracks from Deezer
|
||||||
* Select audio quality: MP3 128kbps, MP3 320kbps (default), or FLAC (⚠️ non-premium accounts are limited to 128kbps)
|
* Select audio quality: MP3 128kbps, MP3 320kbps (default), or FLAC (⚠️ non-premium accounts are limited to 128kbps)
|
||||||
* Automatically adds metadata tags to downloaded files
|
* Automatically adds metadata tags to downloaded files
|
||||||
* Fetch and tag songs with BPM and musical key
|
* Fetch and tag songs with BPM and musical key
|
||||||
@@ -122,6 +122,7 @@ Available Commands:
|
|||||||
album Download songs from an album
|
album Download songs from an album
|
||||||
artist Download top songs from an artist
|
artist Download top songs from an artist
|
||||||
playlist Download songs from a playlist
|
playlist Download songs from a playlist
|
||||||
|
track Download a single track
|
||||||
|
|
||||||
Flags:
|
Flags:
|
||||||
--bpm fetch BPM/key and add to file tags
|
--bpm fetch BPM/key and add to file tags
|
||||||
@@ -134,6 +135,27 @@ Flags:
|
|||||||
Use "godeez download [command] --help" for more information about a command.
|
Use "godeez download [command] --help" for more information about a command.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
Here are some examples of how to use the different download commands:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Download an album
|
||||||
|
godeez download album 12345678
|
||||||
|
|
||||||
|
# Download a playlist
|
||||||
|
godeez download playlist 87654321
|
||||||
|
|
||||||
|
# Download top tracks from an artist
|
||||||
|
godeez download artist 11223344 --limit 5
|
||||||
|
|
||||||
|
# Download a single track
|
||||||
|
godeez download track 98765432
|
||||||
|
|
||||||
|
# Download with specific quality and BPM data
|
||||||
|
godeez download track 98765432 --quality flac --bpm
|
||||||
|
```
|
||||||
|
|
||||||
## Contributing
|
## Contributing
|
||||||
|
|
||||||
Contributions help make **GoDeez** a better tool for everyone, and any help is greatly appreciated.
|
Contributions help make **GoDeez** a better tool for everyone, and any help is greatly appreciated.
|
||||||
|
|||||||
+6
-2
@@ -35,6 +35,7 @@ func init() {
|
|||||||
newDownloadCmd("album"),
|
newDownloadCmd("album"),
|
||||||
newDownloadCmd("playlist"),
|
newDownloadCmd("playlist"),
|
||||||
newDownloadCmd("artist"),
|
newDownloadCmd("artist"),
|
||||||
|
newDownloadCmd("track"),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,9 +78,12 @@ func newDownloadCmd(resourceType string) *cobra.Command {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
if resourceType == "artist" {
|
switch resourceType {
|
||||||
cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of songs to download")
|
case "artist":
|
||||||
cmd.Short = "Download top songs from an artist"
|
cmd.Short = "Download top songs from an artist"
|
||||||
|
cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of songs to download")
|
||||||
|
case "track":
|
||||||
|
cmd.Short = "Download a single track"
|
||||||
}
|
}
|
||||||
|
|
||||||
return cmd
|
return cmd
|
||||||
|
|||||||
@@ -45,6 +45,8 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
|
|||||||
payload["alb_id"] = id
|
payload["alb_id"] = id
|
||||||
case *Artist:
|
case *Artist:
|
||||||
payload["art_id"] = id
|
payload["art_id"] = id
|
||||||
|
case *Track:
|
||||||
|
payload["sng_id"] = id
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported resource type: %T", r)
|
return fmt.Errorf("unsupported resource type: %T", r)
|
||||||
}
|
}
|
||||||
@@ -75,15 +77,17 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(string(body), `"DATA_ERROR":"playlist::getData"`) {
|
switch {
|
||||||
|
case strings.Contains(string(body), `"DATA_ERROR":"playlist::getData"`):
|
||||||
return fmt.Errorf("invalid playlist ID")
|
return fmt.Errorf("invalid playlist ID")
|
||||||
}
|
case strings.Contains(string(body), `"DATA_ERROR":"album::getData"`):
|
||||||
if strings.Contains(string(body), `"DATA_ERROR":"album::getData"`) {
|
|
||||||
return fmt.Errorf("invalid album ID")
|
return fmt.Errorf("invalid album ID")
|
||||||
}
|
case strings.Contains(string(body), `"DATA_ERROR":"artist::getData"`):
|
||||||
if strings.Contains(string(body), `"DATA_ERROR":"artist::getData"`) {
|
|
||||||
return fmt.Errorf("invalid artist ID")
|
return fmt.Errorf("invalid artist ID")
|
||||||
|
case strings.Contains(string(body), `"DATA_ERROR":"song::getData"`):
|
||||||
|
return fmt.Errorf("invalid track ID")
|
||||||
}
|
}
|
||||||
|
|
||||||
if strings.Contains(string(body), `"results":{}`) {
|
if strings.Contains(string(body), `"results":{}`) {
|
||||||
return fmt.Errorf("unexpected response")
|
return fmt.Errorf("unexpected response")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package deezer
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/flytam/filenamify"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Track struct {
|
||||||
|
Results struct {
|
||||||
|
Data *Song `json:"DATA"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) String() string {
|
||||||
|
if t.Results.Data == nil {
|
||||||
|
return "Track: No data available"
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := "Unknown"
|
||||||
|
if t.Results.Data.Duration != "" {
|
||||||
|
if d, err := time.ParseDuration(t.Results.Data.Duration + "s"); err == nil {
|
||||||
|
duration = d.String()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
`================= [ Track Info ] =================
|
||||||
|
Title: %s
|
||||||
|
Artist: %s
|
||||||
|
Duration: %s
|
||||||
|
==================================================`,
|
||||||
|
t.Results.Data.GetTitle(),
|
||||||
|
t.Results.Data.Artist,
|
||||||
|
duration,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) GetType() string {
|
||||||
|
return "Track"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) GetTitle() string {
|
||||||
|
if t.Results.Data == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return t.Results.Data.GetTitle()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) GetSongs() []*Song {
|
||||||
|
if t.Results.Data == nil {
|
||||||
|
return []*Song{}
|
||||||
|
}
|
||||||
|
return []*Song{t.Results.Data}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) SetSongs(songs []*Song) {}
|
||||||
|
|
||||||
|
func (t *Track) GetOutputDir(outputDir string) string {
|
||||||
|
if t.Results.Data == nil {
|
||||||
|
return outputDir
|
||||||
|
}
|
||||||
|
|
||||||
|
// For single tracks, create a simple "Singles" folder
|
||||||
|
base := "Singles"
|
||||||
|
base, _ = filenamify.Filenamify(base, filenamify.Options{})
|
||||||
|
outputDir = path.Join(outputDir, base)
|
||||||
|
|
||||||
|
return outputDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *Track) Unmarshal(data []byte) error {
|
||||||
|
return json.Unmarshal(data, t)
|
||||||
|
}
|
||||||
@@ -63,6 +63,8 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
|
|||||||
resource = &deezer.Playlist{}
|
resource = &deezer.Playlist{}
|
||||||
case "artist":
|
case "artist":
|
||||||
resource = &deezer.Artist{}
|
resource = &deezer.Artist{}
|
||||||
|
case "track":
|
||||||
|
resource = &deezer.Track{}
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported resource type: %s", c.resourceType)
|
return fmt.Errorf("unsupported resource type: %s", c.resourceType)
|
||||||
}
|
}
|
||||||
@@ -73,6 +75,9 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
|
|||||||
|
|
||||||
songs := resource.GetSongs()
|
songs := resource.GetSongs()
|
||||||
if len(songs) == 0 {
|
if len(songs) == 0 {
|
||||||
|
if c.resourceType == "track" {
|
||||||
|
return fmt.Errorf("track with ID %s not found", id)
|
||||||
|
}
|
||||||
return fmt.Errorf("%s has no songs", c.resourceType)
|
return fmt.Errorf("%s has no songs", c.resourceType)
|
||||||
}
|
}
|
||||||
if c.resourceType == "artist" && len(songs) > opts.Limit {
|
if c.resourceType == "artist" && len(songs) > opts.Limit {
|
||||||
@@ -87,7 +92,9 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
fmt.Printf("%s\n\nStarting download...\n\n", resource)
|
if c.resourceType != "track" {
|
||||||
|
fmt.Printf("%s\n\nStarting download...\n\n", resource)
|
||||||
|
}
|
||||||
|
|
||||||
downloaded := 0
|
downloaded := 0
|
||||||
skipped := 0
|
skipped := 0
|
||||||
@@ -146,7 +153,9 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
|
|||||||
if downloaded > 0 || failed > 0 {
|
if downloaded > 0 || failed > 0 {
|
||||||
c.Logger.Infof("Playlist %s (%s): %d downloaded, %d skipped, %d failed\n", resource.GetTitle(), id, downloaded, skipped, failed)
|
c.Logger.Infof("Playlist %s (%s): %d downloaded, %d skipped, %d failed\n", resource.GetTitle(), id, downloaded, skipped, failed)
|
||||||
}
|
}
|
||||||
fmt.Printf(`
|
|
||||||
|
if c.resourceType != "track" {
|
||||||
|
fmt.Printf(`
|
||||||
================== [ Summary ] ==================
|
================== [ Summary ] ==================
|
||||||
Downloaded: %d
|
Downloaded: %d
|
||||||
Skipped: %d
|
Skipped: %d
|
||||||
@@ -155,12 +164,13 @@ Elapsed time: %s
|
|||||||
Files saved to: %s
|
Files saved to: %s
|
||||||
=================================================
|
=================================================
|
||||||
`,
|
`,
|
||||||
downloaded,
|
downloaded,
|
||||||
skipped,
|
skipped,
|
||||||
failed,
|
failed,
|
||||||
time.Since(startTime).Round(time.Second),
|
time.Since(startTime).Round(time.Second),
|
||||||
resourceOutputDir,
|
resourceOutputDir,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user