diff --git a/cmd/download.go b/cmd/download.go index 2284d41..ad33671 100644 --- a/cmd/download.go +++ b/cmd/download.go @@ -20,7 +20,7 @@ var opts downloader.Options var downloadCmd = &cobra.Command{ Use: "download", - Short: "Download songs from Deezer", + Short: "Download tracks from Deezer", Annotations: map[string]string{updateNoticeAnnotation: "true"}, } @@ -31,7 +31,7 @@ func init() { downloadCmd.PersistentFlags().DurationVarP(&opts.Timeout, "timeout", "t", 2*time.Minute, "timeout for each download (e.g. 10s, 1m, 2m30s)") downloadCmd.PersistentFlags().BoolVar(&opts.BPM, "bpm", false, "fetch BPM/key and add to file tags") downloadCmd.PersistentFlags().BoolVar(&opts.Genre, "genre", false, "fetch genre and add to file tags") - downloadCmd.PersistentFlags().BoolVar(&opts.Strict, "strict", false, "fail the song download if the quality is not available") + downloadCmd.PersistentFlags().BoolVar(&opts.Strict, "strict", false, "fail the download if the requested quality is unavailable") downloadCmd.AddCommand( newDownloadCmd("album"), @@ -68,7 +68,7 @@ func newDownloadCmd(resourceType string) *cobra.Command { } if resourceType == "artist" { - cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of songs to download") + cmd.Flags().IntVarP(&opts.Limit, "limit", "l", 10, "number of tracks to download") } return cmd @@ -77,12 +77,12 @@ func newDownloadCmd(resourceType string) *cobra.Command { func downloadShort(resourceType string) string { switch resourceType { case "artist": - return "Download top songs from an artist" + return "Download an artist's top tracks" case "track": return "Download a single track" case "album": - return "Download songs from an album" + return "Download tracks from an album" default: - return fmt.Sprintf("Download songs from a %s", resourceType) + return fmt.Sprintf("Download tracks from a %s", resourceType) } } diff --git a/internal/crypto/blowfish.go b/internal/crypto/blowfish.go index dc588d7..e0d3088 100644 --- a/internal/crypto/blowfish.go +++ b/internal/crypto/blowfish.go @@ -13,8 +13,8 @@ var ( blowfishSecretKey = []byte("g4el58wc0zvf9na1") ) -func GetBlowfishKey(songID string) []byte { - hash := md5.Sum([]byte(songID)) +func GetBlowfishKey(trackID string) []byte { + hash := md5.Sum([]byte(trackID)) hashHex := hex.EncodeToString(hash[:]) key := make([]byte, len(blowfishSecretKey)) diff --git a/internal/deezer/album.go b/internal/deezer/album.go index 8af454a..d31ffdc 100644 --- a/internal/deezer/album.go +++ b/internal/deezer/album.go @@ -22,8 +22,8 @@ type Album struct { Copyright string `json:"COPYRIGHT"` Duration string `json:"DURATION"` } `json:"DATA"` - Songs struct { - Data []*Song `json:"data"` + Tracks struct { + Data []*Track `json:"data"` } `json:"SONGS"` } `json:"results"` } @@ -43,7 +43,7 @@ Duration: %s ==================================================`, a.Results.Data.Title, a.Results.Data.Artist, - len(a.Results.Songs.Data), + len(a.Results.Tracks.Data), time.Duration(duration)*time.Second, ) } @@ -56,12 +56,12 @@ func (a *Album) GetTitle() string { return a.Results.Data.Title } -func (a *Album) GetSongs() []*Song { - return a.Results.Songs.Data +func (a *Album) GetTracks() []*Track { + return a.Results.Tracks.Data } -func (a *Album) SetSongs(s []*Song) { - a.Results.Songs.Data = s +func (a *Album) SetTracks(t []*Track) { + a.Results.Tracks.Data = t } func (a *Album) GetOutputDir(outputDir string) string { diff --git a/internal/deezer/artist.go b/internal/deezer/artist.go index 7f30ccb..80c5a5c 100644 --- a/internal/deezer/artist.go +++ b/internal/deezer/artist.go @@ -16,19 +16,19 @@ type Artist struct { Data struct { Name string `json:"ART_NAME"` } `json:"DATA"` - Songs struct { - Data []*Song `json:"data"` + Tracks struct { + Data []*Track `json:"data"` } `json:"TOP"` } `json:"results"` } func (a *Artist) String() string { - songs := a.Results.Songs.Data - count := len(songs) + tracks := a.Results.Tracks.Data + count := len(tracks) totalSec := 0 - for _, s := range songs { - if d, err := strconv.Atoi(s.Duration); err == nil { + for _, t := range tracks { + if d, err := strconv.Atoi(t.Duration); err == nil { totalSec += d } } @@ -43,8 +43,8 @@ func (a *Artist) String() string { fmt.Fprintf(&b, "-------------------------------------------\n") fmt.Fprintf(&b, "Top %d most popular tracks:\n", limit) for i := 0; i < limit; i++ { - s := songs[i] - fmt.Fprintf(&b, " %2d. %s – %s\n", i+1, s.Artist, s.GetTitle()) + t := tracks[i] + fmt.Fprintf(&b, " %2d. %s – %s\n", i+1, t.Artist, t.GetTitle()) } fmt.Fprintf(&b, "===========================================\n") @@ -59,12 +59,12 @@ func (a *Artist) GetTitle() string { return a.Results.Data.Name } -func (a *Artist) GetSongs() []*Song { - return a.Results.Songs.Data +func (a *Artist) GetTracks() []*Track { + return a.Results.Tracks.Data } -func (a *Artist) SetSongs(s []*Song) { - a.Results.Songs.Data = s +func (a *Artist) SetTracks(t []*Track) { + a.Results.Tracks.Data = t } func (a *Artist) GetOutputDir(outputDir string) string { diff --git a/internal/deezer/client.go b/internal/deezer/client.go index f3faab9..e18e87d 100644 --- a/internal/deezer/client.go +++ b/internal/deezer/client.go @@ -79,7 +79,7 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string idKey = "alb_id" case *Artist: idKey = "art_id" - case *Track: + case *Single: idKey = "sng_id" default: return fmt.Errorf("unsupported resource type: %T", resource) @@ -134,14 +134,14 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string return resource.Unmarshal(body) } -func (c *Client) FetchMedia(ctx context.Context, song *Song, quality string) (*Media, error) { +func (c *Client) FetchMedia(ctx context.Context, track *Track, quality string) (*Media, error) { qualityFormats := map[string]string{ "mp3_128": `[{"cipher":"BF_CBC_STRIPE","format":"MP3_128"}]`, "mp3_320": `[{"cipher":"BF_CBC_STRIPE","format":"MP3_320"},{"cipher":"BF_CBC_STRIPE","format":"MP3_128"}]`, "flac": `[{"cipher":"BF_CBC_STRIPE","format":"FLAC"},{"cipher":"BF_CBC_STRIPE","format":"MP3_320"},{"cipher":"BF_CBC_STRIPE","format":"MP3_128"}]`, } - reqBody := fmt.Sprintf(`{"license_token":"%s","media":[{"type":"FULL","formats":%s}],"track_tokens":["%s"]}`, c.Session.LicenseToken, qualityFormats[quality], song.TrackToken) + reqBody := fmt.Sprintf(`{"license_token":"%s","media":[{"type":"FULL","formats":%s}],"track_tokens":["%s"]}`, c.Session.LicenseToken, qualityFormats[quality], track.TrackToken) req, err := http.NewRequestWithContext(ctx, "POST", "https://media.deezer.com/v1/get_url", bytes.NewBuffer([]byte(reqBody))) if err != nil { return nil, err @@ -188,8 +188,8 @@ func (c *Client) FetchMedia(ctx context.Context, song *Song, quality string) (*M return &media, nil } -func (c *Client) FetchCoverImage(ctx context.Context, song *Song) ([]byte, error) { - url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", song.Cover) +func (c *Client) FetchCoverImage(ctx context.Context, track *Track) ([]byte, error) { + url := fmt.Sprintf("https://e-cdn-images.dzcdn.net/images/cover/%s/500x500-000000-80-0-0.jpg", track.Cover) req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err diff --git a/internal/deezer/playlist.go b/internal/deezer/playlist.go index 5ce75f4..f7b768e 100644 --- a/internal/deezer/playlist.go +++ b/internal/deezer/playlist.go @@ -16,8 +16,8 @@ type Playlist struct { Creator string `json:"PARENT_USERNAME"` Duration int `json:"DURATION"` } `json:"DATA"` - Songs struct { - Data []*Song `json:"data"` + Tracks struct { + Data []*Track `json:"data"` } `json:"SONGS"` } `json:"results"` } @@ -32,7 +32,7 @@ Duration: %s =================================================`, p.Results.Data.Title, p.Results.Data.Creator, - len(p.Results.Songs.Data), + len(p.Results.Tracks.Data), time.Duration(p.Results.Data.Duration)*time.Second, ) } @@ -45,12 +45,12 @@ func (p *Playlist) GetTitle() string { return p.Results.Data.Title } -func (p *Playlist) GetSongs() []*Song { - return p.Results.Songs.Data +func (p *Playlist) GetTracks() []*Track { + return p.Results.Tracks.Data } -func (p *Playlist) SetSongs(s []*Song) { - p.Results.Songs.Data = s +func (p *Playlist) SetTracks(t []*Track) { + p.Results.Tracks.Data = t } func (p *Playlist) GetOutputDir(outputDir string) string { diff --git a/internal/deezer/resource.go b/internal/deezer/resource.go index 53a45e1..8407d05 100644 --- a/internal/deezer/resource.go +++ b/internal/deezer/resource.go @@ -3,8 +3,8 @@ package deezer type Resource interface { GetTitle() string GetType() string - GetSongs() []*Song - SetSongs(songs []*Song) + GetTracks() []*Track + SetTracks(tracks []*Track) GetOutputDir(outputDir string) string Unmarshal(data []byte) error } diff --git a/internal/deezer/single.go b/internal/deezer/single.go new file mode 100644 index 0000000..b1bd08c --- /dev/null +++ b/internal/deezer/single.go @@ -0,0 +1,65 @@ +package deezer + +import ( + "encoding/json" + "fmt" + "path" + "strconv" + "time" +) + +type Single struct { + Results struct { + Data *Track `json:"DATA"` + } `json:"results"` +} + +func (s *Single) String() string { + if s.Results.Data == nil { + return "Track: No data available" + } + + duration, err := strconv.Atoi(s.Results.Data.Duration) + if err != nil { + duration = 0 + } + + return fmt.Sprintf( + `================= [ Track Info ] ================= +Title: %s +Artist: %s +Duration: %s +==================================================`, + s.Results.Data.GetTitle(), + s.Results.Data.Artist, + time.Duration(duration)*time.Second, + ) +} + +func (s *Single) GetType() string { + return "Track" +} + +func (s *Single) GetTitle() string { + if s.Results.Data == nil { + return "" + } + return s.Results.Data.GetTitle() +} + +func (s *Single) GetTracks() []*Track { + if s.Results.Data == nil { + return nil + } + return []*Track{s.Results.Data} +} + +func (s *Single) SetTracks(tracks []*Track) {} + +func (s *Single) GetOutputDir(outputDir string) string { + return path.Join(outputDir, "Singles") +} + +func (s *Single) Unmarshal(data []byte) error { + return json.Unmarshal(data, s) +} diff --git a/internal/deezer/song.go b/internal/deezer/song.go deleted file mode 100644 index f98ef45..0000000 --- a/internal/deezer/song.go +++ /dev/null @@ -1,68 +0,0 @@ -package deezer - -import ( - "encoding/json" - "fmt" - "strconv" - - "github.com/flytam/filenamify" -) - -type Contributors struct { - MainArtists []string `json:"main_artist"` - Composers []string `json:"composer"` - Authors []string `json:"author"` -} - -func (c *Contributors) UnmarshalJSON(data []byte) error { - if string(data) == "[]" { - *c = Contributors{} - return nil - } - - type Alias Contributors - aux := (*Alias)(c) - - return json.Unmarshal(data, aux) -} - -type Song struct { - ID string `json:"SNG_ID"` - Artist string `json:"ART_NAME"` - Title string `json:"SNG_TITLE"` - Version string `json:"VERSION"` - Cover string `json:"ALB_PICTURE"` - Contributors Contributors `json:"SNG_CONTRIBUTORS"` - Duration string `json:"DURATION"` - Gain string `json:"GAIN"` - ISRC string `json:"ISRC"` - TrackNumber string `json:"TRACK_NUMBER"` - TrackToken string `json:"TRACK_TOKEN"` -} - -func (s *Song) GetTitle() string { - if s.Version != "" { - return s.Title + " " + s.Version - } - return s.Title -} - -func (s *Song) GetFileName(resourceType, mediaFormat string) string { - ext := "mp3" - if mediaFormat == "FLAC" { - ext = "flac" - } - - prefix := "" - if resourceType == "album" { - if n, err := strconv.Atoi(s.TrackNumber); err == nil { - prefix = fmt.Sprintf("%02d. ", n) - } else { - prefix = s.TrackNumber + ". " - } - } - - fileName := fmt.Sprintf("%s%s - %s.%s", prefix, s.Artist, s.GetTitle(), ext) - fileName, _ = filenamify.Filenamify(fileName, filenamify.Options{MaxLength: 255}) - return fileName -} diff --git a/internal/deezer/track.go b/internal/deezer/track.go index c464b0c..015886c 100644 --- a/internal/deezer/track.go +++ b/internal/deezer/track.go @@ -3,63 +3,66 @@ package deezer import ( "encoding/json" "fmt" - "path" "strconv" - "time" + + "github.com/flytam/filenamify" ) +type Contributors struct { + MainArtists []string `json:"main_artist"` + Composers []string `json:"composer"` + Authors []string `json:"author"` +} + +func (c *Contributors) UnmarshalJSON(data []byte) error { + if string(data) == "[]" { + *c = Contributors{} + return nil + } + + type Alias Contributors + aux := (*Alias)(c) + + return json.Unmarshal(data, aux) +} + type Track struct { - Results struct { - Data *Song `json:"DATA"` - } `json:"results"` -} - -func (t *Track) String() string { - if t.Results.Data == nil { - return "Track: No data available" - } - - duration, err := strconv.Atoi(t.Results.Data.Duration) - if err != nil { - duration = 0 - } - - return fmt.Sprintf( - `================= [ Track Info ] ================= -Title: %s -Artist: %s -Duration: %s -==================================================`, - t.Results.Data.GetTitle(), - t.Results.Data.Artist, - time.Duration(duration)*time.Second, - ) -} - -func (t *Track) GetType() string { - return "Track" + ID string `json:"SNG_ID"` + Artist string `json:"ART_NAME"` + Title string `json:"SNG_TITLE"` + Version string `json:"VERSION"` + Cover string `json:"ALB_PICTURE"` + Contributors Contributors `json:"SNG_CONTRIBUTORS"` + Duration string `json:"DURATION"` + Gain string `json:"GAIN"` + ISRC string `json:"ISRC"` + TrackNumber string `json:"TRACK_NUMBER"` + TrackToken string `json:"TRACK_TOKEN"` } func (t *Track) GetTitle() string { - if t.Results.Data == nil { - return "" + if t.Version != "" { + return t.Title + " " + t.Version } - return t.Results.Data.GetTitle() + return t.Title } -func (t *Track) GetSongs() []*Song { - if t.Results.Data == nil { - return nil +func (t *Track) GetFileName(resourceType, mediaFormat string) string { + ext := "mp3" + if mediaFormat == "FLAC" { + ext = "flac" } - return []*Song{t.Results.Data} -} -func (t *Track) SetSongs(songs []*Song) {} + prefix := "" + if resourceType == "album" { + if n, err := strconv.Atoi(t.TrackNumber); err == nil { + prefix = fmt.Sprintf("%02d. ", n) + } else { + prefix = t.TrackNumber + ". " + } + } -func (t *Track) GetOutputDir(outputDir string) string { - return path.Join(outputDir, "Singles") -} - -func (t *Track) Unmarshal(data []byte) error { - return json.Unmarshal(data, t) + fileName := fmt.Sprintf("%s%s - %s.%s", prefix, t.Artist, t.GetTitle(), ext) + fileName, _ = filenamify.Filenamify(fileName, filenamify.Options{MaxLength: 255}) + return fileName } diff --git a/internal/downloader/client.go b/internal/downloader/client.go index 0c1d561..e2b77aa 100644 --- a/internal/downloader/client.go +++ b/internal/downloader/client.go @@ -51,7 +51,7 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error { return err } - return c.downloadAllSongs(ctx, resource, id, opts, outputDir) + return c.downloadAllTracks(ctx, resource, id, opts, outputDir) } func (c *Client) initDeezerClient(ctx context.Context, opts Options) error { @@ -78,16 +78,16 @@ func (c *Client) prepareResource(ctx context.Context, id string, opts Options) ( return nil, "", fmt.Errorf("failed to fetch resource: %w", err) } - songs := resource.GetSongs() - if len(songs) == 0 { + tracks := resource.GetTracks() + if len(tracks) == 0 { if c.resourceType == "track" { return nil, "", fmt.Errorf("track with ID %s not found", id) } - return nil, "", fmt.Errorf("%s has no songs", c.resourceType) + return nil, "", fmt.Errorf("%s has no tracks", c.resourceType) } - if c.resourceType == "artist" && len(songs) > opts.Limit { - resource.SetSongs(songs[:opts.Limit]) + if c.resourceType == "artist" && len(tracks) > opts.Limit { + resource.SetTracks(tracks[:opts.Limit]) } outputDir := resource.GetOutputDir(c.appConfig.OutputDir) @@ -107,36 +107,36 @@ func (c *Client) createResource() (deezer.Resource, error) { case "artist": return &deezer.Artist{}, nil case "track": - return &deezer.Track{}, nil + return &deezer.Single{}, nil default: return nil, fmt.Errorf("unsupported resource type: %s", c.resourceType) } } -func (c *Client) downloadAllSongs(ctx context.Context, resource deezer.Resource, resourceID string, opts Options, outputDir string) error { - songs := resource.GetSongs() +func (c *Client) downloadAllTracks(ctx context.Context, resource deezer.Resource, resourceID string, opts Options, outputDir string) error { + tracks := resource.GetTracks() startTime := time.Now() if c.resourceType != "track" { fmt.Printf("%s\n\nStarting download...\n\n", resource) } - progress := newProgressTracker(c.Logger, len(songs), c.resourceType) + progress := newProgressTracker(c.Logger, len(tracks), c.resourceType) - for i, song := range songs { + for i, track := range tracks { if ctx.Err() != nil { return ctx.Err() } - sp := progress.startDownload(i, song) - result := c.downloadSong(ctx, resource, song, opts, outputDir) + sp := progress.startDownload(i, track) + result := c.downloadTrack(ctx, resource, track, opts, outputDir) sp.Stop() if result.err != nil && errors.Is(result.err, context.Canceled) { return result.err } - progress.handleResult(i, song, result) + progress.handleResult(i, track, result) } progress.printSummary(resource.GetTitle(), resourceID, outputDir, time.Since(startTime)) @@ -144,8 +144,8 @@ func (c *Client) downloadAllSongs(ctx context.Context, resource deezer.Resource, return nil } -func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, song *deezer.Song, opts Options, outputDir string) downloadResult { - media, err := c.deezerClient.FetchMedia(ctx, song, opts.Quality) +func (c *Client) downloadTrack(ctx context.Context, resource deezer.Resource, track *deezer.Track, opts Options, outputDir string) downloadResult { + media, err := c.deezerClient.FetchMedia(ctx, track, opts.Quality) if err != nil { return downloadResult{err: fmt.Errorf("failed to fetch media: %w", err)} } @@ -155,13 +155,13 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son return downloadResult{err: fmt.Errorf("requested quality '%s' not available", opts.Quality)} } - if skipPath, skip := c.shouldSkipDownload(ctx, song.ID, mediaFormat); skip { + if skipPath, skip := c.shouldSkipDownload(ctx, track.ID, mediaFormat); skip { return downloadResult{skipped: true, path: skipPath} } metadataChan := make(chan metadataResult, 1) go func() { - metadataChan <- fetchMetadata(c.deezerClient.Session.HttpClient, ctx, song, opts) + metadataChan <- fetchMetadata(c.deezerClient.Session.HttpClient, ctx, track, opts) }() stream, err := c.deezerClient.GetMediaStream(ctx, media) @@ -172,10 +172,10 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout) defer cancel() - fileName := song.GetFileName(c.resourceType, mediaFormat) + fileName := track.GetFileName(c.resourceType, mediaFormat) outputPath := path.Join(outputDir, fileName) - key := crypto.GetBlowfishKey(song.ID) + key := crypto.GetBlowfishKey(track.ID) if err := c.streamToFile(dlCtx, stream, outputPath, key); err != nil { fileutil.DeleteFile(outputPath) return downloadResult{err: fmt.Errorf("failed to stream to file: %w", err)} @@ -187,14 +187,14 @@ func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, son warnings = append(warnings, fmt.Sprintf("requested quality '%s' not available, using '%s' instead", opts.Quality, strings.ToLower(mediaFormat))) } - cover, err := c.deezerClient.FetchCoverImage(ctx, song) + cover, err := c.deezerClient.FetchCoverImage(ctx, track) if err != nil && !errors.Is(err, context.Canceled) { warnings = append(warnings, fmt.Sprintf("failed to fetch cover image: %v", err)) } metadata := <-metadataChan warnings = append(warnings, metadata.warnings...) - warnings = append(warnings, c.finalizeDownload(resource, song, outputPath, mediaFormat, metadata.genre, cover, metadata.bpmKey)...) + warnings = append(warnings, c.finalizeDownload(resource, track, outputPath, mediaFormat, metadata.genre, cover, metadata.bpmKey)...) return downloadResult{warnings: warnings} } @@ -251,10 +251,10 @@ func (c *Client) streamToFile(ctx context.Context, stream io.ReadCloser, outputP return nil } -func (c *Client) finalizeDownload(resource deezer.Resource, song *deezer.Song, outputPath, mediaFormat, genre string, cover []byte, bpmKey bpmKey) []string { +func (c *Client) finalizeDownload(resource deezer.Resource, track *deezer.Track, outputPath, mediaFormat, genre string, cover []byte, bpmKey bpmKey) []string { var warnings []string - if err := tags.AddTags(resource, song, cover, outputPath, bpmKey.BPM, bpmKey.Key, genre); err != nil { + if err := tags.AddTags(resource, track, cover, outputPath, bpmKey.BPM, bpmKey.Key, genre); err != nil { warnings = append(warnings, fmt.Sprintf("failed to add tags: %v", err)) } @@ -264,7 +264,7 @@ func (c *Client) finalizeDownload(resource deezer.Resource, song *deezer.Song, o } info := &store.DownloadInfo{ - SongID: song.ID, + TrackID: track.ID, Quality: mediaFormat, Path: outputPath, Hash: hash, diff --git a/internal/downloader/metadata.go b/internal/downloader/metadata.go index 71e3aef..5fb71d6 100644 --- a/internal/downloader/metadata.go +++ b/internal/downloader/metadata.go @@ -21,7 +21,7 @@ type metadataResult struct { warnings []string } -func fetchMetadata(httpClient *http.Client, ctx context.Context, song *deezer.Song, opts Options) metadataResult { +func fetchMetadata(httpClient *http.Client, ctx context.Context, track *deezer.Track, opts Options) metadataResult { if !opts.BPM && !opts.Genre { return metadataResult{} } @@ -40,14 +40,14 @@ func fetchMetadata(httpClient *http.Client, ctx context.Context, song *deezer.So if opts.BPM { go func() { - result, err := provider.FetchBPM(ctx, httpClient, song.Artist, song.Title, song.Duration) + result, err := provider.FetchBPM(ctx, httpClient, track.Artist, track.Title, track.Duration) bpmChan <- bpmResult{value: bpmKey{BPM: result.BPM, Key: result.Key}, err: err} }() } if opts.Genre { go func() { - genre, err := provider.FetchGenre(ctx, httpClient, song.Artist, song.GetTitle()) + genre, err := provider.FetchGenre(ctx, httpClient, track.Artist, track.GetTitle()) genreChan <- genreResult{value: genre, err: err} }() } diff --git a/internal/downloader/progress.go b/internal/downloader/progress.go index a4aad6f..9c8e768 100644 --- a/internal/downloader/progress.go +++ b/internal/downloader/progress.go @@ -28,46 +28,46 @@ type downloadStats struct { type progressTracker struct { logger *logger.Logger stats downloadStats - totalSongs int + totalTracks int resourceType string } -func newProgressTracker(logger *logger.Logger, totalSongs int, resourceType string) *progressTracker { +func newProgressTracker(logger *logger.Logger, totalTracks int, resourceType string) *progressTracker { return &progressTracker{ logger: logger, - totalSongs: totalSongs, + totalTracks: totalTracks, resourceType: resourceType, } } -func (pt *progressTracker) startDownload(index int, song *deezer.Song) *spinner.Spinner { - trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalSongs) +func (pt *progressTracker) startDownload(index int, track *deezer.Track) *spinner.Spinner { + trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalTracks) sp := spinner.New(spinner.CharSets[14], 100*time.Millisecond) sp.Writer = os.Stdout sp.Prefix = trackProgress + " " - sp.Suffix = fmt.Sprintf(" Downloading: %s - %s", song.Artist, song.GetTitle()) + sp.Suffix = fmt.Sprintf(" Downloading: %s - %s", track.Artist, track.GetTitle()) sp.Start() return sp } -func (pt *progressTracker) handleResult(index int, song *deezer.Song, result downloadResult) { - trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalSongs) - songTitle := song.GetTitle() +func (pt *progressTracker) handleResult(index int, track *deezer.Track, result downloadResult) { + trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalTracks) + trackTitle := track.GetTitle() if result.skipped { pt.stats.skipped++ fmt.Printf("%s ↷ Skipped: %s - %s\n Already exists at: %s\n", - trackProgress, song.Artist, songTitle, result.path) + trackProgress, track.Artist, trackTitle, result.path) return } if result.err != nil { pt.stats.failed++ - pt.logger.Errorf("Failed to download %s - %s: %v\n", song.Artist, songTitle, result.err) + pt.logger.Errorf("Failed to download %s - %s: %v\n", track.Artist, trackTitle, result.err) fmt.Printf("%s ✖ Failed: %s - %s:\n Error: %v\n", - trackProgress, song.Artist, songTitle, result.err) + trackProgress, track.Artist, trackTitle, result.err) return } @@ -75,13 +75,13 @@ func (pt *progressTracker) handleResult(index int, song *deezer.Song, result dow if len(result.warnings) > 0 { pt.stats.warnings++ } - pt.logger.Infof("Downloaded %s - %s\n", song.Artist, songTitle) + pt.logger.Infof("Downloaded %s - %s\n", track.Artist, trackTitle) symbol := "✔" if len(result.warnings) > 0 { symbol = "⚠" } - fmt.Printf("%s %s Downloaded: %s - %s\n", trackProgress, symbol, song.Artist, songTitle) + fmt.Printf("%s %s Downloaded: %s - %s\n", trackProgress, symbol, track.Artist, trackTitle) for _, w := range result.warnings { pt.logger.Warnf("Warning: %s\n", w) @@ -123,8 +123,8 @@ Files saved to: %s } } -func (pt *progressTracker) showSupportMessage() { +func (*progressTracker) showSupportMessage() { if rand.Float64() < 0.1 { - fmt.Printf("\n💖 Enjoying GoDeez? Give us a ⭐ on GitHub: https://github.com/mathismqn/godeez\n") + fmt.Println("\n⭐ Enjoying GoDeez? Star it on GitHub: https://github.com/mathismqn/godeez") } } diff --git a/internal/downloader/skip.go b/internal/downloader/skip.go index 9239a86..802cc04 100644 --- a/internal/downloader/skip.go +++ b/internal/downloader/skip.go @@ -7,8 +7,8 @@ import ( "github.com/mathismqn/godeez/internal/store" ) -func (c *Client) shouldSkipDownload(ctx context.Context, songID, mediaFormat string) (string, bool) { - existing, err := store.GetDownloadInfo(songID) +func (c *Client) shouldSkipDownload(ctx context.Context, trackID, mediaFormat string) (string, bool) { + existing, err := store.GetDownloadInfo(trackID) if err != nil || existing.Quality != mediaFormat { return "", false } diff --git a/internal/provider/bpm.go b/internal/provider/bpm.go index edad4f3..ef5119f 100644 --- a/internal/provider/bpm.go +++ b/internal/provider/bpm.go @@ -26,12 +26,12 @@ var ( ) func FetchBPM(ctx context.Context, httpClient *http.Client, artist, title, duration string) (BPMKey, error) { - songURL, err := findSongURL(ctx, httpClient, artist, title, duration) + trackURL, err := findTrackURL(ctx, httpClient, artist, title, duration) if err != nil { return BPMKey{}, err } - html, err := fetchBPMPage(ctx, httpClient, songURL) + html, err := fetchBPMPage(ctx, httpClient, trackURL) if err != nil { return BPMKey{}, err } @@ -39,7 +39,7 @@ func FetchBPM(ctx context.Context, httpClient *http.Client, artist, title, durat return parseBPM(html) } -func findSongURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) { +func findTrackURL(ctx context.Context, httpClient *http.Client, artist, title, duration string) (string, error) { const rootURL = "https://songbpm.com" values := neturl.Values{} diff --git a/internal/store/download_info.go b/internal/store/download_info.go index 70588b6..5758323 100644 --- a/internal/store/download_info.go +++ b/internal/store/download_info.go @@ -9,7 +9,7 @@ import ( ) type DownloadInfo struct { - SongID string `json:"song_id"` + TrackID string `json:"song_id"` Quality string `json:"quality"` Path string `json:"path"` Hash string `json:"hash"` @@ -18,7 +18,7 @@ type DownloadInfo struct { var trackBucket = []byte("tracks") -func GetDownloadInfo(songID string) (*DownloadInfo, error) { +func GetDownloadInfo(trackID string) (*DownloadInfo, error) { var info DownloadInfo if err := db.View(func(tx *bbolt.Tx) error { @@ -27,7 +27,7 @@ func GetDownloadInfo(songID string) (*DownloadInfo, error) { return fmt.Errorf("bucket not found") } - data := b.Get([]byte(songID)) + data := b.Get([]byte(trackID)) if data == nil { return fmt.Errorf("not found") } @@ -51,6 +51,6 @@ func (d *DownloadInfo) Save() error { return err } - return b.Put([]byte(d.SongID), data) + return b.Put([]byte(d.TrackID), data) }) } diff --git a/internal/tags/flac.go b/internal/tags/flac.go index 2c3401b..b5c7849 100644 --- a/internal/tags/flac.go +++ b/internal/tags/flac.go @@ -16,13 +16,13 @@ type flacTagger struct { index int } -func (t *flacTagger) addTags(resource deezer.Resource, song *deezer.Song, cover []byte, path, tempo, key, genre string) error { +func (t *flacTagger) addTags(resource deezer.Resource, track *deezer.Track, cover []byte, path, tempo, key, genre string) error { if album, ok := resource.(*deezer.Album); ok { if parts := strings.Split(album.Results.Data.PhysicalReleaseDate, "-"); len(parts) == 3 { album.Results.Data.PhysicalReleaseDate = parts[0] } - t.addTag("TRACKNUMBER", song.TrackNumber) + t.addTag("TRACKNUMBER", track.TrackNumber) t.addTag("ALBUMARTIST", album.Results.Data.Artist) t.addTag("ALBUM", album.Results.Data.Title) t.addTag("PUBLISHER", album.Results.Data.Label) @@ -32,13 +32,13 @@ func (t *flacTagger) addTags(resource deezer.Resource, song *deezer.Song, cover t.addTag("COPYRIGHT", album.Results.Data.Copyright) } - t.addTag("ARTIST", strings.Join(song.Contributors.MainArtists, ", ")) - t.addTag("TITLE", song.GetTitle()) - t.addTag("COMPOSER", strings.Join(song.Contributors.Composers, ", ")) - t.addTag("LYRICIST", strings.Join(song.Contributors.Authors, ", ")) + t.addTag("ARTIST", strings.Join(track.Contributors.MainArtists, ", ")) + t.addTag("TITLE", track.GetTitle()) + t.addTag("COMPOSER", strings.Join(track.Contributors.Composers, ", ")) + t.addTag("LYRICIST", strings.Join(track.Contributors.Authors, ", ")) t.addTag("GENRE", genre) - t.addTag("REPLAYGAIN_TRACK_GAIN", song.Gain) - t.addTag("ISRC", song.ISRC) + t.addTag("REPLAYGAIN_TRACK_GAIN", track.Gain) + t.addTag("ISRC", track.ISRC) t.addTag("BPM", tempo) t.addTag("KEY", key) t.addTag("INITIALKEY", key) diff --git a/internal/tags/id3v2.go b/internal/tags/id3v2.go index 5fd52c8..ca40d10 100644 --- a/internal/tags/id3v2.go +++ b/internal/tags/id3v2.go @@ -13,17 +13,17 @@ type id3v2Tagger struct { tag *id3v2.Tag } -func (t *id3v2Tagger) addTags(resource deezer.Resource, song *deezer.Song, cover []byte, path, tempo, key, genre string) error { +func (t *id3v2Tagger) addTags(resource deezer.Resource, track *deezer.Track, cover []byte, path, tempo, key, genre string) error { defer t.tag.Close() - duration, err := strconv.Atoi(song.Duration) + duration, err := strconv.Atoi(track.Duration) if err != nil { return err } - song.Duration = fmt.Sprintf("%d", duration*1000) + track.Duration = fmt.Sprintf("%d", duration*1000) if album, ok := resource.(*deezer.Album); ok { - t.addTag("TRCK", song.TrackNumber) + t.addTag("TRCK", track.TrackNumber) t.addTag("TPE2", album.Results.Data.Artist) t.addTag("TALB", album.Results.Data.Title) t.addTag("TPUB", album.Results.Data.Label) @@ -33,16 +33,16 @@ func (t *id3v2Tagger) addTags(resource deezer.Resource, song *deezer.Song, cover t.addTag("TCOP", album.Results.Data.Copyright) } - t.addTag("TPE1", strings.Join(song.Contributors.MainArtists, ", ")) - t.addTag("TIT2", song.GetTitle()) - t.addTag("TCOM", strings.Join(song.Contributors.Composers, ", ")) - t.addTag("TEXT", strings.Join(song.Contributors.Authors, ", ")) + t.addTag("TPE1", strings.Join(track.Contributors.MainArtists, ", ")) + t.addTag("TIT2", track.GetTitle()) + t.addTag("TCOM", strings.Join(track.Contributors.Composers, ", ")) + t.addTag("TEXT", strings.Join(track.Contributors.Authors, ", ")) t.addTag("TCON", genre) - t.addTag("TLEN", song.Duration) + t.addTag("TLEN", track.Duration) t.addTag("TBPM", tempo) t.addTag("TKEY", key) - t.addTXXX("GAIN", song.Gain) - t.addTXXX("ISRC", song.ISRC) + t.addTXXX("GAIN", track.Gain) + t.addTXXX("ISRC", track.ISRC) t.tag.AddAttachedPicture(id3v2.PictureFrame{ Encoding: t.tag.DefaultEncoding(), diff --git a/internal/tags/tags.go b/internal/tags/tags.go index 789d57d..8a927fd 100644 --- a/internal/tags/tags.go +++ b/internal/tags/tags.go @@ -10,7 +10,7 @@ import ( ) type tagger interface { - addTags(resource deezer.Resource, song *deezer.Song, cover []byte, filePath, tempo, key, genre string) error + addTags(resource deezer.Resource, track *deezer.Track, cover []byte, filePath, tempo, key, genre string) error } func newTagger(filePath string) (tagger, error) { @@ -35,10 +35,10 @@ func newTagger(filePath string) (tagger, error) { return &flacTagger{file: file, cmts: cmts, index: idx}, nil } -func AddTags(resource deezer.Resource, song *deezer.Song, cover []byte, filePath, tempo, key, genre string) error { +func AddTags(resource deezer.Resource, track *deezer.Track, cover []byte, filePath, tempo, key, genre string) error { t, err := newTagger(filePath) if err != nil { return err } - return t.addTags(resource, song, cover, filePath, tempo, key, genre) + return t.addTags(resource, track, cover, filePath, tempo, key, genre) }