refactor: introduce typed resource kind
This commit is contained in:
+16
-15
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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) {
|
||||||
|
|||||||
@@ -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 {
|
func (p *Playlist) GetTitle() string {
|
||||||
return p.Results.Data.Title
|
return p.Results.Data.Title
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 ""
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user