feat: download single tracks (#5)

Introduces the `godeez download track <ID>` command, allowing users to download individual songs directly.

Single tracks are saved into a "Singles" folder by default to keep them organized. The README has been updated with documentation and examples for this new functionality.
This commit is contained in:
Felipe Marinho
2025-09-10 10:26:58 +02:00
committed by GitHub
parent 3022e226ce
commit 9b23e3a8d9
5 changed files with 116 additions and 1 deletions
+23 -1
View File
@@ -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.
+5
View File
@@ -35,6 +35,7 @@ func init() {
newDownloadCmd("album"), newDownloadCmd("album"),
newDownloadCmd("playlist"), newDownloadCmd("playlist"),
newDownloadCmd("artist"), newDownloadCmd("artist"),
newDownloadCmd("track"),
) )
} }
@@ -82,5 +83,9 @@ func newDownloadCmd(resourceType string) *cobra.Command {
cmd.Short = "Download top songs from an artist" cmd.Short = "Download top songs from an artist"
} }
if resourceType == "track" {
cmd.Short = "Download a single track"
}
return cmd return cmd
} }
+5
View File
@@ -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)
} }
@@ -84,6 +86,9 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
if 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")
} }
if 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")
} }
+81
View File
@@ -0,0 +1,81 @@
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) {
if len(songs) > 0 {
t.Results.Data = songs[0]
}
}
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)
}
+2
View File
@@ -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)
} }