refactor: introduce typed resource kind
This commit is contained in:
+16
-15
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,10 +48,6 @@ Duration: %s
|
||||
)
|
||||
}
|
||||
|
||||
func (a *Album) GetType() string {
|
||||
return "Album"
|
||||
}
|
||||
|
||||
func (a *Album) GetTitle() string {
|
||||
return a.Results.Data.Title
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
+20
-25
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -37,10 +37,6 @@ Duration: %s
|
||||
)
|
||||
}
|
||||
|
||||
func (p *Playlist) GetType() string {
|
||||
return "Playlist"
|
||||
}
|
||||
|
||||
func (p *Playlist) GetTitle() string {
|
||||
return p.Results.Data.Title
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package deezer
|
||||
|
||||
type Resource interface {
|
||||
GetTitle() string
|
||||
GetType() string
|
||||
GetTracks() []*Track
|
||||
SetTracks(tracks []*Track)
|
||||
GetOutputDir(outputDir string) string
|
||||
|
||||
@@ -36,10 +36,6 @@ Duration: %s
|
||||
)
|
||||
}
|
||||
|
||||
func (s *Single) GetType() string {
|
||||
return "Track"
|
||||
}
|
||||
|
||||
func (s *Single) GetTitle() string {
|
||||
if s.Results.Data == nil {
|
||||
return ""
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
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)
|
||||
|
||||
@@ -27,13 +27,13 @@ type downloadStats struct {
|
||||
type progressTracker struct {
|
||||
stats downloadStats
|
||||
totalTracks int
|
||||
resourceType string
|
||||
kind deezer.Kind
|
||||
}
|
||||
|
||||
func newProgressTracker(totalTracks int, resourceType string) *progressTracker {
|
||||
func newProgressTracker(totalTracks int, kind deezer.Kind) *progressTracker {
|
||||
return &progressTracker{
|
||||
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) {
|
||||
if pt.resourceType != "track" {
|
||||
if pt.kind != deezer.KindTrack {
|
||||
warningsLine := ""
|
||||
if pt.stats.warnings > 0 {
|
||||
warningsLine = fmt.Sprintf("\nWarnings: %d", pt.stats.warnings)
|
||||
|
||||
Reference in New Issue
Block a user