From 714d56760318a9844fad86879e9993f906943c6b Mon Sep 17 00:00:00 2001 From: Mathis Maquenne <124215603+mathismqn@users.noreply.github.com> Date: Wed, 5 Aug 2026 11:33:13 +0200 Subject: [PATCH] refactor: introduce typed resource kind --- cmd/download.go | 31 +++++++++--------- internal/deezer/album.go | 4 --- internal/deezer/artist.go | 4 --- internal/deezer/client.go | 45 ++++++++++++-------------- internal/deezer/kind.go | 57 +++++++++++++++++++++++++++++++++ internal/deezer/playlist.go | 4 --- internal/deezer/resource.go | 1 - internal/deezer/single.go | 4 --- internal/deezer/track.go | 4 +-- internal/downloader/client.go | 41 +++++++----------------- internal/downloader/progress.go | 14 ++++---- 11 files changed, 113 insertions(+), 96 deletions(-) create mode 100644 internal/deezer/kind.go diff --git a/cmd/download.go b/cmd/download.go index ad33671..24873f6 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -8,6 +8,7 @@ import ( "time" "github.com/mathismqn/godeez/internal/config" + "github.com/mathismqn/godeez/internal/deezer" "github.com/mathismqn/godeez/internal/downloader" "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.AddCommand( - newDownloadCmd("album"), - newDownloadCmd("playlist"), - newDownloadCmd("artist"), - newDownloadCmd("track"), + newDownloadCmd(deezer.KindAlbum), + newDownloadCmd(deezer.KindPlaylist), + newDownloadCmd(deezer.KindArtist), + newDownloadCmd(deezer.KindTrack), ) } -func newDownloadCmd(resourceType string) *cobra.Command { +func newDownloadCmd(kind deezer.Kind) *cobra.Command { cmd := &cobra.Command{ - Use: fmt.Sprintf("%s <%s_id>", resourceType, resourceType), - Short: downloadShort(resourceType), + Use: fmt.Sprintf("%s <%s_id>", kind, kind), + Short: downloadShort(kind), Args: cobra.ExactArgs(1), PreRunE: func(cmd *cobra.Command, args []string) error { appConfig, err := config.New() @@ -59,7 +60,7 @@ func newDownloadCmd(resourceType string) *cobra.Command { RunE: func(cmd *cobra.Command, args []string) error { 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) { 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") } return cmd } -func downloadShort(resourceType string) string { - switch resourceType { - case "artist": +func downloadShort(kind deezer.Kind) string { + switch kind { + case deezer.KindArtist: return "Download an artist's top tracks" - case "track": + case deezer.KindTrack: return "Download a single track" - case "album": + case deezer.KindAlbum: return "Download tracks from an album" default: - return fmt.Sprintf("Download tracks from a %s", resourceType) + return fmt.Sprintf("Download tracks from a %s", kind) } } diff --git a/internal/deezer/album.go b/internal/deezer/album.go index d31ffdc..9faa477 100644 --- a/internal/deezer/album.go +++ b/internal/deezer/album.go @@ -48,10 +48,6 @@ Duration: %s ) } -func (a *Album) GetType() string { - return "Album" -} - func (a *Album) GetTitle() string { return a.Results.Data.Title } diff --git a/internal/deezer/artist.go b/internal/deezer/artist.go index 80c5a5c..af04803 100644 --- a/internal/deezer/artist.go +++ b/internal/deezer/artist.go @@ -51,10 +51,6 @@ func (a *Artist) String() string { return b.String() } -func (a *Artist) GetType() string { - return "Artist" -} - func (a *Artist) GetTitle() string { return a.Results.Data.Name } diff --git a/internal/deezer/client.go b/internal/deezer/client.go index 5e9dda1..b316896 100644 --- a/internal/deezer/client.go +++ b/internal/deezer/client.go @@ -59,7 +59,12 @@ func resolveSession(ctx context.Context, appConfig *config.Config) (*Session, er 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{}{ "nb": 10000, "start": 0, @@ -68,46 +73,32 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string "tags": true, "header": true, } - - 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 + payload[kind.idKey()] = id jsonData, err := json.Marshal(payload) 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)) if err != nil { - return err + return nil, err } resp, err := c.Session.HttpClient.Do(req) if err != nil { - return err + return nil, err } defer resp.Body.Close() 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) if err != nil { - return err + return nil, err } 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"}, } { if strings.Contains(bodyStr, check.marker) { - return fmt.Errorf("%s", check.errMsg) + return nil, fmt.Errorf("%s", check.errMsg) } } 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) { diff --git a/internal/deezer/kind.go b/internal/deezer/kind.go new file mode 100644 index 0000000..b17986d --- /dev/null +++ b/internal/deezer/kind.go @@ -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) +} diff --git a/internal/deezer/playlist.go b/internal/deezer/playlist.go index f7b768e..6b35156 100644 --- a/internal/deezer/playlist.go +++ b/internal/deezer/playlist.go @@ -37,10 +37,6 @@ Duration: %s ) } -func (p *Playlist) GetType() string { - return "Playlist" -} - func (p *Playlist) GetTitle() string { return p.Results.Data.Title } diff --git a/internal/deezer/resource.go b/internal/deezer/resource.go index 8407d05..3efefa2 100644 --- a/internal/deezer/resource.go +++ b/internal/deezer/resource.go @@ -2,7 +2,6 @@ package deezer type Resource interface { GetTitle() string - GetType() string GetTracks() []*Track SetTracks(tracks []*Track) GetOutputDir(outputDir string) string diff --git a/internal/deezer/single.go b/internal/deezer/single.go index b1bd08c..9afb118 100644 --- a/internal/deezer/single.go +++ b/internal/deezer/single.go @@ -36,10 +36,6 @@ Duration: %s ) } -func (s *Single) GetType() string { - return "Track" -} - func (s *Single) GetTitle() string { if s.Results.Data == nil { return "" diff --git a/internal/deezer/track.go b/internal/deezer/track.go index 015886c..79098c2 100644 --- a/internal/deezer/track.go +++ b/internal/deezer/track.go @@ -47,14 +47,14 @@ func (t *Track) GetTitle() string { return t.Title } -func (t *Track) GetFileName(resourceType, mediaFormat string) string { +func (t *Track) Filename(kind Kind, mediaFormat string) string { ext := "mp3" if mediaFormat == "FLAC" { ext = "flac" } prefix := "" - if resourceType == "album" { + if kind == KindAlbum { if n, err := strconv.Atoi(t.TrackNumber); err == nil { prefix = fmt.Sprintf("%02d. ", n) } else { diff --git a/internal/downloader/client.go b/internal/downloader/client.go index 1cfe282..31df887 100644 --- a/internal/downloader/client.go +++ b/internal/downloader/client.go @@ -23,7 +23,7 @@ const chunkSize = 2048 type Client struct { appConfig *config.Config - resourceType string + kind deezer.Kind deezerClient *deezer.Client hashIndexOnce sync.Once @@ -31,10 +31,10 @@ type Client struct { hashIndexErr error } -func New(appConfig *config.Config, resourceType string) *Client { +func New(appConfig *config.Config, kind deezer.Kind) *Client { return &Client{ - appConfig: appConfig, - resourceType: resourceType, + appConfig: appConfig, + 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) { - resource, err := c.createResource() + resource, err := c.deezerClient.FetchResource(ctx, c.kind, id) 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) } tracks := resource.GetTracks() 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("%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]) } @@ -95,30 +91,15 @@ func (c *Client) prepareResource(ctx context.Context, id string, opts Options) ( 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 { tracks := resource.GetTracks() startTime := time.Now() - if c.resourceType != "track" { + if c.kind != deezer.KindTrack { 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 { 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) defer cancel() - fileName := track.GetFileName(c.resourceType, mediaFormat) + fileName := track.Filename(c.kind, mediaFormat) outputPath := path.Join(outputDir, fileName) key := crypto.GetBlowfishKey(track.ID) diff --git a/internal/downloader/progress.go b/internal/downloader/progress.go index e2c4af6..c451dc1 100644 --- a/internal/downloader/progress.go +++ b/internal/downloader/progress.go @@ -25,15 +25,15 @@ type downloadStats struct { } type progressTracker struct { - stats downloadStats - totalTracks int - resourceType string + stats downloadStats + totalTracks int + kind deezer.Kind } -func newProgressTracker(totalTracks int, resourceType string) *progressTracker { +func newProgressTracker(totalTracks int, kind deezer.Kind) *progressTracker { return &progressTracker{ - totalTracks: totalTracks, - resourceType: resourceType, + totalTracks: totalTracks, + 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) { - if pt.resourceType != "track" { + if pt.kind != deezer.KindTrack { warningsLine := "" if pt.stats.warnings > 0 { warningsLine = fmt.Sprintf("\nWarnings: %d", pt.stats.warnings)