Files

108 lines
2.9 KiB
Go

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"`
}
// UnmarshalJSON tolerates the empty array Deezer sends when a track has no
// contributors. The field is an object in every other case, so decoding it
// straight into the struct fails on those tracks.
func (c *Contributors) UnmarshalJSON(data []byte) error {
if string(data) == "[]" {
*c = Contributors{}
return nil
}
// Alias drops the method set, so this Unmarshal does not recurse.
type Alias Contributors
aux := (*Alias)(c)
return json.Unmarshal(data, aux)
}
type Track 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 (t *Track) FullTitle() string {
if t.Version != "" {
return t.Title + " " + t.Version
}
return t.Title
}
// Filename builds the on-disk name for the track, sanitised for the current
// filesystem. Album downloads get a zero padded track number prefix so the
// directory sorts in playing order; the other kinds have no meaningful
// ordering to preserve.
func (t *Track) Filename(kind Kind, format string) string {
ext := "mp3"
switch format {
case "FLAC":
ext = "flac"
case "WAV":
ext = "wav"
}
prefix := ""
if kind == KindAlbum {
if n, err := strconv.Atoi(t.TrackNumber); err == nil {
prefix = fmt.Sprintf("%02d. ", n)
} else {
prefix = t.TrackNumber + ". "
}
}
base := fmt.Sprintf("%s%s - %s", prefix, t.Artist, t.FullTitle())
base, _ = filenamify.Filenamify(base, filenamify.Options{MaxLength: 255})
// 255 bytes is the per-component limit on ext4 and APFS. The budget also
// has to cover the extension, its dot, and the "-id3v2" suffix the tagging
// library appends to its temporary file: without that headroom, tagging a
// long title fails after the download has already succeeded.
base = truncateBytes(base, 255-len(ext)-1-len("-id3v2"))
return base + "." + ext
}
// truncateBytes shortens s to at most maxLen bytes without splitting a rune.
// The limit is in bytes because that is what filesystems enforce, but cutting
// mid-rune would leave an invalid UTF-8 name, so it backs up to the last rune
// boundary that fits.
func truncateBytes(s string, maxLen int) string {
if maxLen <= 0 {
return ""
}
if len(s) <= maxLen {
return s
}
last := 0
for i := range s {
if i > maxLen {
return s[:last]
}
last = i
}
return s[:last]
}