refactor: introduce typed resource kind

This commit is contained in:
Mathis Maquenne
2026-08-05 11:33:13 +02:00
parent 3ec258b876
commit 714d567603
11 changed files with 113 additions and 96 deletions
+16 -15
View File
@@ -8,6 +8,7 @@ import (
"time" "time"
"github.com/mathismqn/godeez/internal/config" "github.com/mathismqn/godeez/internal/config"
"github.com/mathismqn/godeez/internal/deezer"
"github.com/mathismqn/godeez/internal/downloader" "github.com/mathismqn/godeez/internal/downloader"
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
@@ -34,17 +35,17 @@ func init() {
downloadCmd.PersistentFlags().BoolVar(&opts.Strict, "strict", false, "fail the download if the requested quality is unavailable") downloadCmd.PersistentFlags().BoolVar(&opts.Strict, "strict", false, "fail the download if the requested quality is unavailable")
downloadCmd.AddCommand( downloadCmd.AddCommand(
newDownloadCmd("album"), newDownloadCmd(deezer.KindAlbum),
newDownloadCmd("playlist"), newDownloadCmd(deezer.KindPlaylist),
newDownloadCmd("artist"), newDownloadCmd(deezer.KindArtist),
newDownloadCmd("track"), newDownloadCmd(deezer.KindTrack),
) )
} }
func newDownloadCmd(resourceType string) *cobra.Command { func newDownloadCmd(kind deezer.Kind) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: fmt.Sprintf("%s <%s_id>", resourceType, resourceType), Use: fmt.Sprintf("%s <%s_id>", kind, kind),
Short: downloadShort(resourceType), Short: downloadShort(kind),
Args: cobra.ExactArgs(1), Args: cobra.ExactArgs(1),
PreRunE: func(cmd *cobra.Command, args []string) error { PreRunE: func(cmd *cobra.Command, args []string) error {
appConfig, err := config.New() appConfig, err := config.New()
@@ -59,7 +60,7 @@ func newDownloadCmd(resourceType string) *cobra.Command {
RunE: func(cmd *cobra.Command, args []string) error { RunE: func(cmd *cobra.Command, args []string) error {
appConfig := cmd.Context().Value(appConfigKey).(*config.Config) appConfig := cmd.Context().Value(appConfigKey).(*config.Config)
err := downloader.New(appConfig, resourceType).Run(cmd.Context(), opts, args[0]) err := downloader.New(appConfig, kind).Run(cmd.Context(), opts, args[0])
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
return nil return nil
} }
@@ -67,22 +68,22 @@ func newDownloadCmd(resourceType string) *cobra.Command {
}, },
} }
if resourceType == "artist" { if kind == deezer.KindArtist {
cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of tracks to download") cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of tracks to download")
} }
return cmd return cmd
} }
func downloadShort(resourceType string) string { func downloadShort(kind deezer.Kind) string {
switch resourceType { switch kind {
case "artist": case deezer.KindArtist:
return "Download an artist's top tracks" return "Download an artist's top tracks"
case "track": case deezer.KindTrack:
return "Download a single track" return "Download a single track"
case "album": case deezer.KindAlbum:
return "Download tracks from an album" return "Download tracks from an album"
default: default:
return fmt.Sprintf("Download tracks from a %s", resourceType) return fmt.Sprintf("Download tracks from a %s", kind)
} }
} }
-4
View File
@@ -48,10 +48,6 @@ Duration: %s
) )
} }
func (a *Album) GetType() string {
return "Album"
}
func (a *Album) GetTitle() string { func (a *Album) GetTitle() string {
return a.Results.Data.Title return a.Results.Data.Title
} }
-4
View File
@@ -51,10 +51,6 @@ func (a *Artist) String() string {
return b.String() return b.String()
} }
func (a *Artist) GetType() string {
return "Artist"
}
func (a *Artist) GetTitle() string { func (a *Artist) GetTitle() string {
return a.Results.Data.Name return a.Results.Data.Name
} }
+20 -25
View File
@@ -59,7 +59,12 @@ func resolveSession(ctx context.Context, appConfig *config.Config) (*Session, er
return session, nil return session, nil
} }
func (c *Client) FetchResource(ctx context.Context, resource Resource, id string) error { func (c *Client) FetchResource(ctx context.Context, kind Kind, id string) (Resource, error) {
resource, err := kind.newResource()
if err != nil {
return nil, err
}
payload := map[string]interface{}{ payload := map[string]interface{}{
"nb": 10000, "nb": 10000,
"start": 0, "start": 0,
@@ -68,46 +73,32 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
"tags": true, "tags": true,
"header": true, "header": true,
} }
payload[kind.idKey()] = id
var idKey string
switch resource.(type) {
case *Playlist:
idKey = "playlist_id"
case *Album:
idKey = "alb_id"
case *Artist:
idKey = "art_id"
case *Single:
idKey = "sng_id"
default:
return fmt.Errorf("unsupported resource type: %T", resource)
}
payload[idKey] = id
jsonData, err := json.Marshal(payload) jsonData, err := json.Marshal(payload)
if err != nil { if err != nil {
return err return nil, 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", resource.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", kind.pageMethod(), c.Session.APIToken)
req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData)) req, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
if err != nil { if err != nil {
return err return nil, err
} }
resp, err := c.Session.HttpClient.Do(req) resp, err := c.Session.HttpClient.Do(req)
if err != nil { if err != nil {
return err return nil, err
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { if resp.StatusCode != http.StatusOK {
return fmt.Errorf("unexpected status code: %d", resp.StatusCode) return nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
} }
body, err := io.ReadAll(resp.Body) body, err := io.ReadAll(resp.Body)
if err != nil { if err != nil {
return err return nil, err
} }
bodyStr := string(body) bodyStr := string(body)
@@ -121,15 +112,19 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
{`"DATA_ERROR":"song::getData"`, "invalid track ID"}, {`"DATA_ERROR":"song::getData"`, "invalid track ID"},
} { } {
if strings.Contains(bodyStr, check.marker) { if strings.Contains(bodyStr, check.marker) {
return fmt.Errorf("%s", check.errMsg) return nil, fmt.Errorf("%s", check.errMsg)
} }
} }
if strings.Contains(bodyStr, `"results":{}`) { if strings.Contains(bodyStr, `"results":{}`) {
return fmt.Errorf("unexpected response") return nil, fmt.Errorf("unexpected response")
} }
return resource.Unmarshal(body) if err := resource.Unmarshal(body); err != nil {
return nil, err
}
return resource, nil
} }
func (c *Client) FetchMedia(ctx context.Context, track *Track, quality string) (*Media, error) { func (c *Client) FetchMedia(ctx context.Context, track *Track, quality string) (*Media, error) {
+57
View File
@@ -0,0 +1,57 @@
package deezer
import "fmt"
type Kind string
const (
KindAlbum Kind = "album"
KindPlaylist Kind = "playlist"
KindArtist Kind = "artist"
KindTrack Kind = "track"
)
func (k Kind) pageMethod() string {
switch k {
case KindAlbum:
return "Album"
case KindPlaylist:
return "Playlist"
case KindArtist:
return "Artist"
case KindTrack:
return "Track"
}
return ""
}
func (k Kind) idKey() string {
switch k {
case KindAlbum:
return "alb_id"
case KindPlaylist:
return "playlist_id"
case KindArtist:
return "art_id"
case KindTrack:
return "sng_id"
}
return ""
}
func (k Kind) newResource() (Resource, error) {
switch k {
case KindAlbum:
return &Album{}, nil
case KindPlaylist:
return &Playlist{}, nil
case KindArtist:
return &Artist{}, nil
case KindTrack:
return &Single{}, nil
}
return nil, fmt.Errorf("unsupported resource type: %s", k)
}
-4
View File
@@ -37,10 +37,6 @@ Duration: %s
) )
} }
func (p *Playlist) GetType() string {
return "Playlist"
}
func (p *Playlist) GetTitle() string { func (p *Playlist) GetTitle() string {
return p.Results.Data.Title return p.Results.Data.Title
} }
-1
View File
@@ -2,7 +2,6 @@ package deezer
type Resource interface { type Resource interface {
GetTitle() string GetTitle() string
GetType() string
GetTracks() []*Track GetTracks() []*Track
SetTracks(tracks []*Track) SetTracks(tracks []*Track)
GetOutputDir(outputDir string) string GetOutputDir(outputDir string) string
-4
View File
@@ -36,10 +36,6 @@ Duration: %s
) )
} }
func (s *Single) GetType() string {
return "Track"
}
func (s *Single) GetTitle() string { func (s *Single) GetTitle() string {
if s.Results.Data == nil { if s.Results.Data == nil {
return "" return ""
+2 -2
View File
@@ -47,14 +47,14 @@ func (t *Track) GetTitle() string {
return t.Title return t.Title
} }
func (t *Track) GetFileName(resourceType, mediaFormat string) string { func (t *Track) Filename(kind Kind, mediaFormat string) string {
ext := "mp3" ext := "mp3"
if mediaFormat == "FLAC" { if mediaFormat == "FLAC" {
ext = "flac" ext = "flac"
} }
prefix := "" prefix := ""
if resourceType == "album" { if kind == KindAlbum {
if n, err := strconv.Atoi(t.TrackNumber); err == nil { if n, err := strconv.Atoi(t.TrackNumber); err == nil {
prefix = fmt.Sprintf("%02d. ", n) prefix = fmt.Sprintf("%02d. ", n)
} else { } else {
+11 -30
View File
@@ -23,7 +23,7 @@ const chunkSize = 2048
type Client struct { type Client struct {
appConfig *config.Config appConfig *config.Config
resourceType string kind deezer.Kind
deezerClient *deezer.Client deezerClient *deezer.Client
hashIndexOnce sync.Once hashIndexOnce sync.Once
@@ -31,10 +31,10 @@ type Client struct {
hashIndexErr error hashIndexErr error
} }
func New(appConfig *config.Config, resourceType string) *Client { func New(appConfig *config.Config, kind deezer.Kind) *Client {
return &Client{ return &Client{
appConfig: appConfig, appConfig: appConfig,
resourceType: resourceType, kind: kind,
} }
} }
@@ -66,24 +66,20 @@ func (c *Client) initDeezerClient(ctx context.Context, opts Options) error {
} }
func (c *Client) prepareResource(ctx context.Context, id string, opts Options) (deezer.Resource, string, error) { func (c *Client) prepareResource(ctx context.Context, id string, opts Options) (deezer.Resource, string, error) {
resource, err := c.createResource() resource, err := c.deezerClient.FetchResource(ctx, c.kind, id)
if err != nil { if err != nil {
return nil, "", err
}
if err := c.deezerClient.FetchResource(ctx, resource, id); err != nil {
return nil, "", fmt.Errorf("failed to fetch resource: %w", err) return nil, "", fmt.Errorf("failed to fetch resource: %w", err)
} }
tracks := resource.GetTracks() tracks := resource.GetTracks()
if len(tracks) == 0 { if len(tracks) == 0 {
if c.resourceType == "track" { if c.kind == deezer.KindTrack {
return nil, "", fmt.Errorf("track with ID %s not found", id) return nil, "", fmt.Errorf("track with ID %s not found", id)
} }
return nil, "", fmt.Errorf("%s has no tracks", c.resourceType) return nil, "", fmt.Errorf("%s has no tracks", c.kind)
} }
if c.resourceType == "artist" && len(tracks) > opts.Limit { if c.kind == deezer.KindArtist && len(tracks) > opts.Limit {
resource.SetTracks(tracks[:opts.Limit]) resource.SetTracks(tracks[:opts.Limit])
} }
@@ -95,30 +91,15 @@ func (c *Client) prepareResource(ctx context.Context, id string, opts Options) (
return resource, outputDir, nil return resource, outputDir, nil
} }
func (c *Client) createResource() (deezer.Resource, error) {
switch c.resourceType {
case "album":
return &deezer.Album{}, nil
case "playlist":
return &deezer.Playlist{}, nil
case "artist":
return &deezer.Artist{}, nil
case "track":
return &deezer.Single{}, nil
default:
return nil, fmt.Errorf("unsupported resource type: %s", c.resourceType)
}
}
func (c *Client) downloadAllTracks(ctx context.Context, resource deezer.Resource, opts Options, outputDir string) error { func (c *Client) downloadAllTracks(ctx context.Context, resource deezer.Resource, opts Options, outputDir string) error {
tracks := resource.GetTracks() tracks := resource.GetTracks()
startTime := time.Now() startTime := time.Now()
if c.resourceType != "track" { if c.kind != deezer.KindTrack {
fmt.Printf("%s\n\nStarting download...\n\n", resource) fmt.Printf("%s\n\nStarting download...\n\n", resource)
} }
progress := newProgressTracker(len(tracks), c.resourceType) progress := newProgressTracker(len(tracks), c.kind)
for i, track := range tracks { for i, track := range tracks {
if ctx.Err() != nil { if ctx.Err() != nil {
@@ -169,7 +150,7 @@ func (c *Client) downloadTrack(ctx context.Context, resource deezer.Resource, tr
dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout) dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout)
defer cancel() defer cancel()
fileName := track.GetFileName(c.resourceType, mediaFormat) fileName := track.Filename(c.kind, mediaFormat)
outputPath := path.Join(outputDir, fileName) outputPath := path.Join(outputDir, fileName)
key := crypto.GetBlowfishKey(track.ID) key := crypto.GetBlowfishKey(track.ID)
+7 -7
View File
@@ -25,15 +25,15 @@ type downloadStats struct {
} }
type progressTracker struct { type progressTracker struct {
stats downloadStats stats downloadStats
totalTracks int totalTracks int
resourceType string kind deezer.Kind
} }
func newProgressTracker(totalTracks int, resourceType string) *progressTracker { func newProgressTracker(totalTracks int, kind deezer.Kind) *progressTracker {
return &progressTracker{ return &progressTracker{
totalTracks: totalTracks, totalTracks: totalTracks,
resourceType: resourceType, kind: kind,
} }
} }
@@ -84,7 +84,7 @@ func (pt *progressTracker) handleResult(index int, track *deezer.Track, result d
} }
func (pt *progressTracker) printSummary(outputDir string, elapsed time.Duration) { func (pt *progressTracker) printSummary(outputDir string, elapsed time.Duration) {
if pt.resourceType != "track" { if pt.kind != deezer.KindTrack {
warningsLine := "" warningsLine := ""
if pt.stats.warnings > 0 { if pt.stats.warnings > 0 {
warningsLine = fmt.Sprintf("\nWarnings: %d", pt.stats.warnings) warningsLine = fmt.Sprintf("\nWarnings: %d", pt.stats.warnings)