refactor: use track terminology instead of song
This commit is contained in:
+6
-6
@@ -20,7 +20,7 @@ var opts downloader.Options
|
|||||||
|
|
||||||
var downloadCmd = &cobra.Command{
|
var downloadCmd = &cobra.Command{
|
||||||
Use: "download",
|
Use: "download",
|
||||||
Short: "Download songs from Deezer",
|
Short: "Download tracks from Deezer",
|
||||||
Annotations: map[string]string{updateNoticeAnnotation: "true"},
|
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().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.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.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(
|
downloadCmd.AddCommand(
|
||||||
newDownloadCmd("album"),
|
newDownloadCmd("album"),
|
||||||
@@ -68,7 +68,7 @@ func newDownloadCmd(resourceType string) *cobra.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resourceType == "artist" {
|
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
|
return cmd
|
||||||
@@ -77,12 +77,12 @@ func newDownloadCmd(resourceType string) *cobra.Command {
|
|||||||
func downloadShort(resourceType string) string {
|
func downloadShort(resourceType string) string {
|
||||||
switch resourceType {
|
switch resourceType {
|
||||||
case "artist":
|
case "artist":
|
||||||
return "Download top songs from an artist"
|
return "Download an artist's top tracks"
|
||||||
case "track":
|
case "track":
|
||||||
return "Download a single track"
|
return "Download a single track"
|
||||||
case "album":
|
case "album":
|
||||||
return "Download songs from an album"
|
return "Download tracks from an album"
|
||||||
default:
|
default:
|
||||||
return fmt.Sprintf("Download songs from a %s", resourceType)
|
return fmt.Sprintf("Download tracks from a %s", resourceType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,8 +13,8 @@ var (
|
|||||||
blowfishSecretKey = []byte("g4el58wc0zvf9na1")
|
blowfishSecretKey = []byte("g4el58wc0zvf9na1")
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetBlowfishKey(songID string) []byte {
|
func GetBlowfishKey(trackID string) []byte {
|
||||||
hash := md5.Sum([]byte(songID))
|
hash := md5.Sum([]byte(trackID))
|
||||||
hashHex := hex.EncodeToString(hash[:])
|
hashHex := hex.EncodeToString(hash[:])
|
||||||
|
|
||||||
key := make([]byte, len(blowfishSecretKey))
|
key := make([]byte, len(blowfishSecretKey))
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ type Album struct {
|
|||||||
Copyright string `json:"COPYRIGHT"`
|
Copyright string `json:"COPYRIGHT"`
|
||||||
Duration string `json:"DURATION"`
|
Duration string `json:"DURATION"`
|
||||||
} `json:"DATA"`
|
} `json:"DATA"`
|
||||||
Songs struct {
|
Tracks struct {
|
||||||
Data []*Song `json:"data"`
|
Data []*Track `json:"data"`
|
||||||
} `json:"SONGS"`
|
} `json:"SONGS"`
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ Duration: %s
|
|||||||
==================================================`,
|
==================================================`,
|
||||||
a.Results.Data.Title,
|
a.Results.Data.Title,
|
||||||
a.Results.Data.Artist,
|
a.Results.Data.Artist,
|
||||||
len(a.Results.Songs.Data),
|
len(a.Results.Tracks.Data),
|
||||||
time.Duration(duration)*time.Second,
|
time.Duration(duration)*time.Second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -56,12 +56,12 @@ func (a *Album) GetTitle() string {
|
|||||||
return a.Results.Data.Title
|
return a.Results.Data.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Album) GetSongs() []*Song {
|
func (a *Album) GetTracks() []*Track {
|
||||||
return a.Results.Songs.Data
|
return a.Results.Tracks.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Album) SetSongs(s []*Song) {
|
func (a *Album) SetTracks(t []*Track) {
|
||||||
a.Results.Songs.Data = s
|
a.Results.Tracks.Data = t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Album) GetOutputDir(outputDir string) string {
|
func (a *Album) GetOutputDir(outputDir string) string {
|
||||||
|
|||||||
+12
-12
@@ -16,19 +16,19 @@ type Artist struct {
|
|||||||
Data struct {
|
Data struct {
|
||||||
Name string `json:"ART_NAME"`
|
Name string `json:"ART_NAME"`
|
||||||
} `json:"DATA"`
|
} `json:"DATA"`
|
||||||
Songs struct {
|
Tracks struct {
|
||||||
Data []*Song `json:"data"`
|
Data []*Track `json:"data"`
|
||||||
} `json:"TOP"`
|
} `json:"TOP"`
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Artist) String() string {
|
func (a *Artist) String() string {
|
||||||
songs := a.Results.Songs.Data
|
tracks := a.Results.Tracks.Data
|
||||||
count := len(songs)
|
count := len(tracks)
|
||||||
|
|
||||||
totalSec := 0
|
totalSec := 0
|
||||||
for _, s := range songs {
|
for _, t := range tracks {
|
||||||
if d, err := strconv.Atoi(s.Duration); err == nil {
|
if d, err := strconv.Atoi(t.Duration); err == nil {
|
||||||
totalSec += d
|
totalSec += d
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ func (a *Artist) String() string {
|
|||||||
fmt.Fprintf(&b, "-------------------------------------------\n")
|
fmt.Fprintf(&b, "-------------------------------------------\n")
|
||||||
fmt.Fprintf(&b, "Top %d most popular tracks:\n", limit)
|
fmt.Fprintf(&b, "Top %d most popular tracks:\n", limit)
|
||||||
for i := 0; i < limit; i++ {
|
for i := 0; i < limit; i++ {
|
||||||
s := songs[i]
|
t := tracks[i]
|
||||||
fmt.Fprintf(&b, " %2d. %s – %s\n", i+1, s.Artist, s.GetTitle())
|
fmt.Fprintf(&b, " %2d. %s – %s\n", i+1, t.Artist, t.GetTitle())
|
||||||
}
|
}
|
||||||
fmt.Fprintf(&b, "===========================================\n")
|
fmt.Fprintf(&b, "===========================================\n")
|
||||||
|
|
||||||
@@ -59,12 +59,12 @@ func (a *Artist) GetTitle() string {
|
|||||||
return a.Results.Data.Name
|
return a.Results.Data.Name
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Artist) GetSongs() []*Song {
|
func (a *Artist) GetTracks() []*Track {
|
||||||
return a.Results.Songs.Data
|
return a.Results.Tracks.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Artist) SetSongs(s []*Song) {
|
func (a *Artist) SetTracks(t []*Track) {
|
||||||
a.Results.Songs.Data = s
|
a.Results.Tracks.Data = t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *Artist) GetOutputDir(outputDir string) string {
|
func (a *Artist) GetOutputDir(outputDir string) string {
|
||||||
|
|||||||
@@ -79,7 +79,7 @@ func (c *Client) FetchResource(ctx context.Context, resource Resource, id string
|
|||||||
idKey = "alb_id"
|
idKey = "alb_id"
|
||||||
case *Artist:
|
case *Artist:
|
||||||
idKey = "art_id"
|
idKey = "art_id"
|
||||||
case *Track:
|
case *Single:
|
||||||
idKey = "sng_id"
|
idKey = "sng_id"
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported resource type: %T", resource)
|
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)
|
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{
|
qualityFormats := map[string]string{
|
||||||
"mp3_128": `[{"cipher":"BF_CBC_STRIPE","format":"MP3_128"}]`,
|
"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"}]`,
|
"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"}]`,
|
"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)))
|
req, err := http.NewRequestWithContext(ctx, "POST", "https://media.deezer.com/v1/get_url", bytes.NewBuffer([]byte(reqBody)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -188,8 +188,8 @@ func (c *Client) FetchMedia(ctx context.Context, song *Song, quality string) (*M
|
|||||||
return &media, nil
|
return &media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) FetchCoverImage(ctx context.Context, song *Song) ([]byte, error) {
|
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", song.Cover)
|
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)
|
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -16,8 +16,8 @@ type Playlist struct {
|
|||||||
Creator string `json:"PARENT_USERNAME"`
|
Creator string `json:"PARENT_USERNAME"`
|
||||||
Duration int `json:"DURATION"`
|
Duration int `json:"DURATION"`
|
||||||
} `json:"DATA"`
|
} `json:"DATA"`
|
||||||
Songs struct {
|
Tracks struct {
|
||||||
Data []*Song `json:"data"`
|
Data []*Track `json:"data"`
|
||||||
} `json:"SONGS"`
|
} `json:"SONGS"`
|
||||||
} `json:"results"`
|
} `json:"results"`
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ Duration: %s
|
|||||||
=================================================`,
|
=================================================`,
|
||||||
p.Results.Data.Title,
|
p.Results.Data.Title,
|
||||||
p.Results.Data.Creator,
|
p.Results.Data.Creator,
|
||||||
len(p.Results.Songs.Data),
|
len(p.Results.Tracks.Data),
|
||||||
time.Duration(p.Results.Data.Duration)*time.Second,
|
time.Duration(p.Results.Data.Duration)*time.Second,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -45,12 +45,12 @@ func (p *Playlist) GetTitle() string {
|
|||||||
return p.Results.Data.Title
|
return p.Results.Data.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Playlist) GetSongs() []*Song {
|
func (p *Playlist) GetTracks() []*Track {
|
||||||
return p.Results.Songs.Data
|
return p.Results.Tracks.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Playlist) SetSongs(s []*Song) {
|
func (p *Playlist) SetTracks(t []*Track) {
|
||||||
p.Results.Songs.Data = s
|
p.Results.Tracks.Data = t
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *Playlist) GetOutputDir(outputDir string) string {
|
func (p *Playlist) GetOutputDir(outputDir string) string {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package deezer
|
|||||||
type Resource interface {
|
type Resource interface {
|
||||||
GetTitle() string
|
GetTitle() string
|
||||||
GetType() string
|
GetType() string
|
||||||
GetSongs() []*Song
|
GetTracks() []*Track
|
||||||
SetSongs(songs []*Song)
|
SetTracks(tracks []*Track)
|
||||||
GetOutputDir(outputDir string) string
|
GetOutputDir(outputDir string) string
|
||||||
Unmarshal(data []byte) error
|
Unmarshal(data []byte) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
+48
-45
@@ -3,63 +3,66 @@ package deezer
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"path"
|
|
||||||
"strconv"
|
"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 {
|
type Track struct {
|
||||||
Results struct {
|
ID string `json:"SNG_ID"`
|
||||||
Data *Song `json:"DATA"`
|
Artist string `json:"ART_NAME"`
|
||||||
} `json:"results"`
|
Title string `json:"SNG_TITLE"`
|
||||||
}
|
Version string `json:"VERSION"`
|
||||||
|
Cover string `json:"ALB_PICTURE"`
|
||||||
func (t *Track) String() string {
|
Contributors Contributors `json:"SNG_CONTRIBUTORS"`
|
||||||
if t.Results.Data == nil {
|
Duration string `json:"DURATION"`
|
||||||
return "Track: No data available"
|
Gain string `json:"GAIN"`
|
||||||
}
|
ISRC string `json:"ISRC"`
|
||||||
|
TrackNumber string `json:"TRACK_NUMBER"`
|
||||||
duration, err := strconv.Atoi(t.Results.Data.Duration)
|
TrackToken string `json:"TRACK_TOKEN"`
|
||||||
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"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) GetTitle() string {
|
func (t *Track) GetTitle() string {
|
||||||
if t.Results.Data == nil {
|
if t.Version != "" {
|
||||||
return ""
|
return t.Title + " " + t.Version
|
||||||
}
|
}
|
||||||
return t.Results.Data.GetTitle()
|
return t.Title
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) GetSongs() []*Song {
|
func (t *Track) GetFileName(resourceType, mediaFormat string) string {
|
||||||
if t.Results.Data == nil {
|
ext := "mp3"
|
||||||
return nil
|
if mediaFormat == "FLAC" {
|
||||||
}
|
ext = "flac"
|
||||||
return []*Song{t.Results.Data}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) SetSongs(songs []*Song) {}
|
prefix := ""
|
||||||
|
if resourceType == "album" {
|
||||||
func (t *Track) GetOutputDir(outputDir string) string {
|
if n, err := strconv.Atoi(t.TrackNumber); err == nil {
|
||||||
return path.Join(outputDir, "Singles")
|
prefix = fmt.Sprintf("%02d. ", n)
|
||||||
|
} else {
|
||||||
|
prefix = t.TrackNumber + ". "
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *Track) Unmarshal(data []byte) error {
|
fileName := fmt.Sprintf("%s%s - %s.%s", prefix, t.Artist, t.GetTitle(), ext)
|
||||||
return json.Unmarshal(data, t)
|
fileName, _ = filenamify.Filenamify(fileName, filenamify.Options{MaxLength: 255})
|
||||||
|
return fileName
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func (c *Client) Run(ctx context.Context, opts Options, id string) error {
|
|||||||
return err
|
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 {
|
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)
|
return nil, "", fmt.Errorf("failed to fetch resource: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
songs := resource.GetSongs()
|
tracks := resource.GetTracks()
|
||||||
if len(songs) == 0 {
|
if len(tracks) == 0 {
|
||||||
if c.resourceType == "track" {
|
if c.resourceType == "track" {
|
||||||
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 songs", c.resourceType)
|
return nil, "", fmt.Errorf("%s has no tracks", c.resourceType)
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.resourceType == "artist" && len(songs) > opts.Limit {
|
if c.resourceType == "artist" && len(tracks) > opts.Limit {
|
||||||
resource.SetSongs(songs[:opts.Limit])
|
resource.SetTracks(tracks[:opts.Limit])
|
||||||
}
|
}
|
||||||
|
|
||||||
outputDir := resource.GetOutputDir(c.appConfig.OutputDir)
|
outputDir := resource.GetOutputDir(c.appConfig.OutputDir)
|
||||||
@@ -107,36 +107,36 @@ func (c *Client) createResource() (deezer.Resource, error) {
|
|||||||
case "artist":
|
case "artist":
|
||||||
return &deezer.Artist{}, nil
|
return &deezer.Artist{}, nil
|
||||||
case "track":
|
case "track":
|
||||||
return &deezer.Track{}, nil
|
return &deezer.Single{}, nil
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported resource type: %s", c.resourceType)
|
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 {
|
func (c *Client) downloadAllTracks(ctx context.Context, resource deezer.Resource, resourceID string, opts Options, outputDir string) error {
|
||||||
songs := resource.GetSongs()
|
tracks := resource.GetTracks()
|
||||||
startTime := time.Now()
|
startTime := time.Now()
|
||||||
|
|
||||||
if c.resourceType != "track" {
|
if c.resourceType != "track" {
|
||||||
fmt.Printf("%s\n\nStarting download...\n\n", resource)
|
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 {
|
if ctx.Err() != nil {
|
||||||
return ctx.Err()
|
return ctx.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
sp := progress.startDownload(i, song)
|
sp := progress.startDownload(i, track)
|
||||||
result := c.downloadSong(ctx, resource, song, opts, outputDir)
|
result := c.downloadTrack(ctx, resource, track, opts, outputDir)
|
||||||
sp.Stop()
|
sp.Stop()
|
||||||
|
|
||||||
if result.err != nil && errors.Is(result.err, context.Canceled) {
|
if result.err != nil && errors.Is(result.err, context.Canceled) {
|
||||||
return result.err
|
return result.err
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.handleResult(i, song, result)
|
progress.handleResult(i, track, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
progress.printSummary(resource.GetTitle(), resourceID, outputDir, time.Since(startTime))
|
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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) downloadSong(ctx context.Context, resource deezer.Resource, song *deezer.Song, opts Options, outputDir string) downloadResult {
|
func (c *Client) downloadTrack(ctx context.Context, resource deezer.Resource, track *deezer.Track, opts Options, outputDir string) downloadResult {
|
||||||
media, err := c.deezerClient.FetchMedia(ctx, song, opts.Quality)
|
media, err := c.deezerClient.FetchMedia(ctx, track, opts.Quality)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return downloadResult{err: fmt.Errorf("failed to fetch media: %w", err)}
|
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)}
|
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}
|
return downloadResult{skipped: true, path: skipPath}
|
||||||
}
|
}
|
||||||
|
|
||||||
metadataChan := make(chan metadataResult, 1)
|
metadataChan := make(chan metadataResult, 1)
|
||||||
go func() {
|
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)
|
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)
|
dlCtx, cancel := context.WithTimeout(ctx, opts.Timeout)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
fileName := song.GetFileName(c.resourceType, mediaFormat)
|
fileName := track.GetFileName(c.resourceType, mediaFormat)
|
||||||
outputPath := path.Join(outputDir, fileName)
|
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 {
|
if err := c.streamToFile(dlCtx, stream, outputPath, key); err != nil {
|
||||||
fileutil.DeleteFile(outputPath)
|
fileutil.DeleteFile(outputPath)
|
||||||
return downloadResult{err: fmt.Errorf("failed to stream to file: %w", err)}
|
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)))
|
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) {
|
if err != nil && !errors.Is(err, context.Canceled) {
|
||||||
warnings = append(warnings, fmt.Sprintf("failed to fetch cover image: %v", err))
|
warnings = append(warnings, fmt.Sprintf("failed to fetch cover image: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
metadata := <-metadataChan
|
metadata := <-metadataChan
|
||||||
warnings = append(warnings, metadata.warnings...)
|
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}
|
return downloadResult{warnings: warnings}
|
||||||
}
|
}
|
||||||
@@ -251,10 +251,10 @@ func (c *Client) streamToFile(ctx context.Context, stream io.ReadCloser, outputP
|
|||||||
return nil
|
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
|
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))
|
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{
|
info := &store.DownloadInfo{
|
||||||
SongID: song.ID,
|
TrackID: track.ID,
|
||||||
Quality: mediaFormat,
|
Quality: mediaFormat,
|
||||||
Path: outputPath,
|
Path: outputPath,
|
||||||
Hash: hash,
|
Hash: hash,
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ type metadataResult struct {
|
|||||||
warnings []string
|
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 {
|
if !opts.BPM && !opts.Genre {
|
||||||
return metadataResult{}
|
return metadataResult{}
|
||||||
}
|
}
|
||||||
@@ -40,14 +40,14 @@ func fetchMetadata(httpClient *http.Client, ctx context.Context, song *deezer.So
|
|||||||
|
|
||||||
if opts.BPM {
|
if opts.BPM {
|
||||||
go func() {
|
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}
|
bpmChan <- bpmResult{value: bpmKey{BPM: result.BPM, Key: result.Key}, err: err}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
if opts.Genre {
|
if opts.Genre {
|
||||||
go func() {
|
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}
|
genreChan <- genreResult{value: genre, err: err}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,46 +28,46 @@ type downloadStats struct {
|
|||||||
type progressTracker struct {
|
type progressTracker struct {
|
||||||
logger *logger.Logger
|
logger *logger.Logger
|
||||||
stats downloadStats
|
stats downloadStats
|
||||||
totalSongs int
|
totalTracks int
|
||||||
resourceType string
|
resourceType string
|
||||||
}
|
}
|
||||||
|
|
||||||
func newProgressTracker(logger *logger.Logger, totalSongs int, resourceType string) *progressTracker {
|
func newProgressTracker(logger *logger.Logger, totalTracks int, resourceType string) *progressTracker {
|
||||||
return &progressTracker{
|
return &progressTracker{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
totalSongs: totalSongs,
|
totalTracks: totalTracks,
|
||||||
resourceType: resourceType,
|
resourceType: resourceType,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pt *progressTracker) startDownload(index int, song *deezer.Song) *spinner.Spinner {
|
func (pt *progressTracker) startDownload(index int, track *deezer.Track) *spinner.Spinner {
|
||||||
trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalSongs)
|
trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalTracks)
|
||||||
|
|
||||||
sp := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
sp := spinner.New(spinner.CharSets[14], 100*time.Millisecond)
|
||||||
sp.Writer = os.Stdout
|
sp.Writer = os.Stdout
|
||||||
sp.Prefix = trackProgress + " "
|
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()
|
sp.Start()
|
||||||
|
|
||||||
return sp
|
return sp
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pt *progressTracker) handleResult(index int, song *deezer.Song, result downloadResult) {
|
func (pt *progressTracker) handleResult(index int, track *deezer.Track, result downloadResult) {
|
||||||
trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalSongs)
|
trackProgress := fmt.Sprintf("[%d/%d]", index+1, pt.totalTracks)
|
||||||
songTitle := song.GetTitle()
|
trackTitle := track.GetTitle()
|
||||||
|
|
||||||
if result.skipped {
|
if result.skipped {
|
||||||
pt.stats.skipped++
|
pt.stats.skipped++
|
||||||
fmt.Printf("%s ↷ Skipped: %s - %s\n Already exists at: %s\n",
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
pt.stats.failed++
|
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",
|
fmt.Printf("%s ✖ Failed: %s - %s:\n Error: %v\n",
|
||||||
trackProgress, song.Artist, songTitle, result.err)
|
trackProgress, track.Artist, trackTitle, result.err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -75,13 +75,13 @@ func (pt *progressTracker) handleResult(index int, song *deezer.Song, result dow
|
|||||||
if len(result.warnings) > 0 {
|
if len(result.warnings) > 0 {
|
||||||
pt.stats.warnings++
|
pt.stats.warnings++
|
||||||
}
|
}
|
||||||
pt.logger.Infof("Downloaded %s - %s\n", song.Artist, songTitle)
|
pt.logger.Infof("Downloaded %s - %s\n", track.Artist, trackTitle)
|
||||||
|
|
||||||
symbol := "✔"
|
symbol := "✔"
|
||||||
if len(result.warnings) > 0 {
|
if len(result.warnings) > 0 {
|
||||||
symbol = "⚠"
|
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 {
|
for _, w := range result.warnings {
|
||||||
pt.logger.Warnf("Warning: %s\n", w)
|
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 {
|
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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"github.com/mathismqn/godeez/internal/store"
|
"github.com/mathismqn/godeez/internal/store"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (c *Client) shouldSkipDownload(ctx context.Context, songID, mediaFormat string) (string, bool) {
|
func (c *Client) shouldSkipDownload(ctx context.Context, trackID, mediaFormat string) (string, bool) {
|
||||||
existing, err := store.GetDownloadInfo(songID)
|
existing, err := store.GetDownloadInfo(trackID)
|
||||||
if err != nil || existing.Quality != mediaFormat {
|
if err != nil || existing.Quality != mediaFormat {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,12 +26,12 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func FetchBPM(ctx context.Context, httpClient *http.Client, artist, title, duration string) (BPMKey, error) {
|
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 {
|
if err != nil {
|
||||||
return BPMKey{}, err
|
return BPMKey{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
html, err := fetchBPMPage(ctx, httpClient, songURL)
|
html, err := fetchBPMPage(ctx, httpClient, trackURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return BPMKey{}, err
|
return BPMKey{}, err
|
||||||
}
|
}
|
||||||
@@ -39,7 +39,7 @@ func FetchBPM(ctx context.Context, httpClient *http.Client, artist, title, durat
|
|||||||
return parseBPM(html)
|
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"
|
const rootURL = "https://songbpm.com"
|
||||||
|
|
||||||
values := neturl.Values{}
|
values := neturl.Values{}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type DownloadInfo struct {
|
type DownloadInfo struct {
|
||||||
SongID string `json:"song_id"`
|
TrackID string `json:"song_id"`
|
||||||
Quality string `json:"quality"`
|
Quality string `json:"quality"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Hash string `json:"hash"`
|
Hash string `json:"hash"`
|
||||||
@@ -18,7 +18,7 @@ type DownloadInfo struct {
|
|||||||
|
|
||||||
var trackBucket = []byte("tracks")
|
var trackBucket = []byte("tracks")
|
||||||
|
|
||||||
func GetDownloadInfo(songID string) (*DownloadInfo, error) {
|
func GetDownloadInfo(trackID string) (*DownloadInfo, error) {
|
||||||
var info DownloadInfo
|
var info DownloadInfo
|
||||||
|
|
||||||
if err := db.View(func(tx *bbolt.Tx) error {
|
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")
|
return fmt.Errorf("bucket not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
data := b.Get([]byte(songID))
|
data := b.Get([]byte(trackID))
|
||||||
if data == nil {
|
if data == nil {
|
||||||
return fmt.Errorf("not found")
|
return fmt.Errorf("not found")
|
||||||
}
|
}
|
||||||
@@ -51,6 +51,6 @@ func (d *DownloadInfo) Save() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return b.Put([]byte(d.SongID), data)
|
return b.Put([]byte(d.TrackID), data)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,13 +16,13 @@ type flacTagger struct {
|
|||||||
index int
|
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 album, ok := resource.(*deezer.Album); ok {
|
||||||
if parts := strings.Split(album.Results.Data.PhysicalReleaseDate, "-"); len(parts) == 3 {
|
if parts := strings.Split(album.Results.Data.PhysicalReleaseDate, "-"); len(parts) == 3 {
|
||||||
album.Results.Data.PhysicalReleaseDate = parts[0]
|
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("ALBUMARTIST", album.Results.Data.Artist)
|
||||||
t.addTag("ALBUM", album.Results.Data.Title)
|
t.addTag("ALBUM", album.Results.Data.Title)
|
||||||
t.addTag("PUBLISHER", album.Results.Data.Label)
|
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("COPYRIGHT", album.Results.Data.Copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.addTag("ARTIST", strings.Join(song.Contributors.MainArtists, ", "))
|
t.addTag("ARTIST", strings.Join(track.Contributors.MainArtists, ", "))
|
||||||
t.addTag("TITLE", song.GetTitle())
|
t.addTag("TITLE", track.GetTitle())
|
||||||
t.addTag("COMPOSER", strings.Join(song.Contributors.Composers, ", "))
|
t.addTag("COMPOSER", strings.Join(track.Contributors.Composers, ", "))
|
||||||
t.addTag("LYRICIST", strings.Join(song.Contributors.Authors, ", "))
|
t.addTag("LYRICIST", strings.Join(track.Contributors.Authors, ", "))
|
||||||
t.addTag("GENRE", genre)
|
t.addTag("GENRE", genre)
|
||||||
t.addTag("REPLAYGAIN_TRACK_GAIN", song.Gain)
|
t.addTag("REPLAYGAIN_TRACK_GAIN", track.Gain)
|
||||||
t.addTag("ISRC", song.ISRC)
|
t.addTag("ISRC", track.ISRC)
|
||||||
t.addTag("BPM", tempo)
|
t.addTag("BPM", tempo)
|
||||||
t.addTag("KEY", key)
|
t.addTag("KEY", key)
|
||||||
t.addTag("INITIALKEY", key)
|
t.addTag("INITIALKEY", key)
|
||||||
|
|||||||
+11
-11
@@ -13,17 +13,17 @@ type id3v2Tagger struct {
|
|||||||
tag *id3v2.Tag
|
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()
|
defer t.tag.Close()
|
||||||
|
|
||||||
duration, err := strconv.Atoi(song.Duration)
|
duration, err := strconv.Atoi(track.Duration)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
song.Duration = fmt.Sprintf("%d", duration*1000)
|
track.Duration = fmt.Sprintf("%d", duration*1000)
|
||||||
|
|
||||||
if album, ok := resource.(*deezer.Album); ok {
|
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("TPE2", album.Results.Data.Artist)
|
||||||
t.addTag("TALB", album.Results.Data.Title)
|
t.addTag("TALB", album.Results.Data.Title)
|
||||||
t.addTag("TPUB", album.Results.Data.Label)
|
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("TCOP", album.Results.Data.Copyright)
|
||||||
}
|
}
|
||||||
|
|
||||||
t.addTag("TPE1", strings.Join(song.Contributors.MainArtists, ", "))
|
t.addTag("TPE1", strings.Join(track.Contributors.MainArtists, ", "))
|
||||||
t.addTag("TIT2", song.GetTitle())
|
t.addTag("TIT2", track.GetTitle())
|
||||||
t.addTag("TCOM", strings.Join(song.Contributors.Composers, ", "))
|
t.addTag("TCOM", strings.Join(track.Contributors.Composers, ", "))
|
||||||
t.addTag("TEXT", strings.Join(song.Contributors.Authors, ", "))
|
t.addTag("TEXT", strings.Join(track.Contributors.Authors, ", "))
|
||||||
t.addTag("TCON", genre)
|
t.addTag("TCON", genre)
|
||||||
t.addTag("TLEN", song.Duration)
|
t.addTag("TLEN", track.Duration)
|
||||||
t.addTag("TBPM", tempo)
|
t.addTag("TBPM", tempo)
|
||||||
t.addTag("TKEY", key)
|
t.addTag("TKEY", key)
|
||||||
t.addTXXX("GAIN", song.Gain)
|
t.addTXXX("GAIN", track.Gain)
|
||||||
t.addTXXX("ISRC", song.ISRC)
|
t.addTXXX("ISRC", track.ISRC)
|
||||||
|
|
||||||
t.tag.AddAttachedPicture(id3v2.PictureFrame{
|
t.tag.AddAttachedPicture(id3v2.PictureFrame{
|
||||||
Encoding: t.tag.DefaultEncoding(),
|
Encoding: t.tag.DefaultEncoding(),
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type tagger interface {
|
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) {
|
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
|
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)
|
t, err := newTagger(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return t.addTags(resource, song, cover, filePath, tempo, key, genre)
|
return t.addTags(resource, track, cover, filePath, tempo, key, genre)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user