feat: download artist's top tracks with --limit flag

This commit is contained in:
Mathis Maquenne
2025-06-20 20:00:59 -04:00
parent 1fdd0e8c19
commit 67b4fe404e
8 changed files with 136 additions and 12 deletions
+4
View File
@@ -59,6 +59,10 @@ func (a *Album) GetSongs() []*Song {
return a.Results.Songs.Data
}
func (a *Album) SetSongs(s []*Song) {
a.Results.Songs.Data = s
}
func (a *Album) GetOutputDir(outputDir string) string {
base := fmt.Sprintf("%s - %s", a.Results.Data.Artist, a.Results.Data.Title)
base, _ = filenamify.Filenamify(base, filenamify.Options{})
+82
View File
@@ -0,0 +1,82 @@
package deezer
import (
"encoding/json"
"fmt"
"path"
"strconv"
"strings"
"time"
"github.com/flytam/filenamify"
)
type Artist struct {
Results struct {
Data struct {
Name string `json:"ART_NAME"`
} `json:"DATA"`
Songs struct {
Data []*Song `json:"data"`
} `json:"TOP"`
} `json:"results"`
}
func (a *Artist) GetType() string {
return "Artist"
}
func (a *Artist) GetTitle() string {
return a.Results.Data.Name
}
func (a *Artist) GetSongs() []*Song {
return a.Results.Songs.Data
}
func (a *Artist) SetSongs(s []*Song) {
a.Results.Songs.Data = s
}
func (a *Artist) GetOutputDir(outputDir string) string {
base, _ := filenamify.Filenamify(a.GetTitle(), filenamify.Options{})
return path.Join(outputDir, base)
}
func (a *Artist) Unmarshal(data []byte) error {
return json.Unmarshal(data, a)
}
func (a *Artist) String() string {
tracks := a.GetSongs()
count := len(tracks)
limit := 3
if count < limit {
limit = count
}
totalSec := 0
for _, s := range tracks {
if d, err := strconv.Atoi(s.Duration); err == nil {
totalSec += d
}
}
totalDuration := time.Duration(totalSec) * time.Second
var b strings.Builder
fmt.Fprintf(&b, "============= [ Artist Info ] =============\n")
fmt.Fprintf(&b, "Artist: %s\n", a.GetTitle())
fmt.Fprintf(&b, "Tracks: %d\n", count)
fmt.Fprintf(&b, "Playtime: %s\n", totalDuration)
fmt.Fprintf(&b, "-------------------------------------------\n")
fmt.Fprintf(&b, "Top %d most popular tracks:\n", limit)
for i := 0; i < limit; i++ {
s := tracks[i]
title := s.GetTitle()
fmt.Fprintf(&b, " %2d. %s %s\n", i+1, s.Artist, title)
}
fmt.Fprintf(&b, "===========================================\n")
return b.String()
}
+23 -11
View File
@@ -29,23 +29,32 @@ func NewClient(ctx context.Context, appConfig *config.Config) (*Client, error) {
}, nil
}
func (c *Client) FetchResource(ctx context.Context, ressource Resource, id string) error {
func (c *Client) FetchResource(ctx context.Context, resource Resource, id string) error {
payload := map[string]interface{}{
"nb": 10000,
"start": 0,
"playlist_id": id,
"alb_id": id,
"lang": "en",
"tab": 0,
"tags": true,
"header": true,
"nb": 10000,
"start": 0,
"lang": "en",
"tab": 0,
"tags": true,
"header": true,
}
switch r := resource.(type) {
case *Playlist:
payload["playlist_id"] = id
case *Album:
payload["alb_id"] = id
case *Artist:
payload["art_id"] = id
default:
return fmt.Errorf("unsupported resource type: %T", r)
}
jsonData, err := json.Marshal(payload)
if err != nil {
return err
}
url := fmt.Sprintf("https://www.deezer.com/ajax/gw-light.php?method=deezer.page%s&input=3&api_version=1.0&api_token=%s", ressource.GetType(), c.Session.APIToken)
url := fmt.Sprintf("https://www.deezer.com/ajax/gw-light.php?method=deezer.page%s&input=3&api_version=1.0&api_token=%s", resource.GetType(), c.Session.APIToken)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil {
return err
@@ -72,11 +81,14 @@ func (c *Client) FetchResource(ctx context.Context, ressource Resource, id strin
if strings.Contains(string(body), `"DATA_ERROR":"album::getData"`) {
return fmt.Errorf("invalid album ID")
}
if strings.Contains(string(body), `"DATA_ERROR":"artist::getData"`) {
return fmt.Errorf("invalid artist ID")
}
if strings.Contains(string(body), `"results":{}`) {
return fmt.Errorf("unexpected response")
}
return ressource.Unmarshal(body)
return resource.Unmarshal(body)
}
func (c *Client) FetchMedia(ctx context.Context, song *Song, quality string) (*Media, error) {
+4
View File
@@ -50,6 +50,10 @@ func (p *Playlist) GetSongs() []*Song {
return p.Results.Songs.Data
}
func (p *Playlist) SetSongs(s []*Song) {
p.Results.Songs.Data = s
}
func (p *Playlist) GetOutputDir(outputDir string) string {
p.Results.Data.Title, _ = filenamify.Filenamify(p.Results.Data.Title, filenamify.Options{})
outputDir = path.Join(outputDir, p.Results.Data.Title)
+1
View File
@@ -4,6 +4,7 @@ type Resource interface {
GetTitle() string
GetType() string
GetSongs() []*Song
SetSongs(songs []*Song)
GetOutputDir(outputDir string) string
Unmarshal(data []byte) error
}
+6
View File
@@ -57,6 +57,8 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
resource = &deezer.Album{}
case "playlist":
resource = &deezer.Playlist{}
case "artist":
resource = &deezer.Artist{}
default:
return fmt.Errorf("unsupported resource type: %s", c.resourceType)
}
@@ -69,6 +71,10 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
if len(songs) == 0 {
return fmt.Errorf("%s has no songs", c.resourceType)
}
if opts.Limit > 0 && len(songs) > opts.Limit {
songs = songs[:opts.Limit]
resource.SetSongs(songs)
}
rootOutputDir := c.appConfig.OutputDir
resourceOutputDir := resource.GetOutputDir(rootOutputDir)
+4
View File
@@ -16,12 +16,16 @@ type Options struct {
Quality string
Timeout time.Duration
BPM bool
Limit int
}
func (o *Options) Validate() error {
if !validQualities[o.Quality] {
return fmt.Errorf("invalid quality option: %s", o.Quality)
}
if o.Limit < 0 {
return fmt.Errorf("limit must be a non-negative integer")
}
return nil
}