Apple Music ALAC / Dolby Atmos Downloader
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

3090 lines
86 KiB

package main
import (
"bufio"
"bytes"
"encoding/binary"
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"math"
"net"
"net/http"
"net/url"
"os"
"os/exec"
"path/filepath"
"regexp"
"sort"
"strconv"
"strings"
"time"
"github.com/spf13/pflag"
"gopkg.in/yaml.v2"
"github.com/abema/go-mp4"
"github.com/beevik/etree"
"github.com/grafov/m3u8"
"github.com/schollz/progressbar/v3"
)
const (
defaultId = "0"
prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1"
)
var (
forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`)
)
var (
dl_atmos bool
dl_select bool
artist_select bool
alac_max *int
atmos_max *int
)
type Config struct {
MediaUserToken string `yaml:"media-user-token"`
AuthorizationToken string `yaml:"authorization-token"`
SaveLrcFile bool `yaml:"save-lrc-file"`
LrcFormat string `yaml:"lrc-format"`
SaveAnimatedArtwork bool `yaml:"save-animated-artwork"`
EmbyAnimatedArtwork bool `yaml:"emby-animated-artwork"`
EmbedLrc bool `yaml:"embed-lrc"`
EmbedCover bool `yaml:"embed-cover"`
SaveArtistCover bool `yaml:"save-artist-cover"`
CoverSize string `yaml:"cover-size"`
CoverFormat string `yaml:"cover-format"`
AlacSaveFolder string `yaml:"alac-save-folder"`
AtmosSaveFolder string `yaml:"atmos-save-folder"`
AlbumFolderFormat string `yaml:"album-folder-format"`
PlaylistFolderFormat string `yaml:"playlist-folder-format"`
ArtistFolderFormat string `yaml:"artist-folder-format"`
SongFileFormat string `yaml:"song-file-format"`
ExplicitChoice string `yaml:"explicit-choice"`
CleanChoice string `yaml:"clean-choice"`
AppleMasterChoice string `yaml:"apple-master-choice"`
ForceApi bool `yaml:"force-api"`
Check string `yaml:"check"`
DecryptM3u8Port string `yaml:"decrypt-m3u8-port"`
GetM3u8Port string `yaml:"get-m3u8-port"`
GetM3u8Mode string `yaml:"get-m3u8-mode"`
GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"`
AlacMax int `yaml:"alac-max"`
AtmosMax int `yaml:"atmos-max"`
LimitMax int `yaml:"limit-max"`
UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"`
DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"`
}
var config Config
var txtpath string
//统计结果
var counter Counter
type Counter struct {
Unavailable int
NotSong int
Error int
Success int
Total int
}
var okDict = make(map[string][]int)
type SampleInfo struct {
data []byte
duration uint32
descIndex uint32
}
type SongInfo struct {
r io.ReadSeeker
alacParam *Alac
samples []SampleInfo
totalDataSize int64
}
func loadConfig() error {
// 读取config.yaml文件内容
data, err := ioutil.ReadFile("config.yaml")
if err != nil {
return err
}
// 将yaml解析到config变量中
err = yaml.Unmarshal(data, &config)
if err != nil {
return err
}
return nil
}
func LimitString(s string) string {
if len([]rune(s)) > config.LimitMax {
return string([]rune(s)[:config.LimitMax])
}
return s
}
func (s *SongInfo) Duration() (ret uint64) {
for i := range s.samples {
ret += uint64(s.samples[i].duration)
}
return
}
func (*Alac) GetType() mp4.BoxType {
return BoxTypeAlac()
}
func isInArray(arr []int, target int) bool {
for _, num := range arr {
if num == target {
return true
}
}
return false
}
func fileExists(path string) (bool, error) {
f, err := os.Stat(path)
if err == nil {
return !f.IsDir(), nil
} else if os.IsNotExist(err) {
return false, nil
}
return false, err
}
func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error {
index := trackNum - 1
{ // ftyp
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()})
if err != nil {
return err
}
_, err = mp4.Marshal(w, &mp4.Ftyp{
MajorBrand: [4]byte{'M', '4', 'A', ' '},
MinorVersion: 0,
CompatibleBrands: []mp4.CompatibleBrandElem{
{CompatibleBrand: [4]byte{'M', '4', 'A', ' '}},
{CompatibleBrand: [4]byte{'m', 'p', '4', '2'}},
{CompatibleBrand: mp4.BrandISOM()},
{CompatibleBrand: [4]byte{0, 0, 0, 0}},
},
}, box.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
const chunkSize uint32 = 5
duration := info.Duration()
numSamples := uint32(len(info.samples))
var stco *mp4.BoxInfo
{ // moov
_, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()})
if err != nil {
return err
}
box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()})
if err != nil {
return err
}
moovOri := box[0]
{ // mvhd
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()})
if err != nil {
return err
}
oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()})
if err != nil {
return err
}
mvhd := oriBox[0].Payload.(*mp4.Mvhd)
if mvhd.Version == 0 {
mvhd.DurationV0 = uint32(duration)
} else {
mvhd.DurationV1 = duration
}
_, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // trak
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()})
if err != nil {
return err
}
box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()})
if err != nil {
return err
}
trakOri := box[0]
{ // tkhd
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()})
if err != nil {
return err
}
oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()})
if err != nil {
return err
}
tkhd := oriBox[0].Payload.(*mp4.Tkhd)
if tkhd.Version == 0 {
tkhd.DurationV0 = uint32(duration)
} else {
tkhd.DurationV1 = duration
}
tkhd.SetFlags(0x7)
_, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // mdia
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()})
if err != nil {
return err
}
box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()})
if err != nil {
return err
}
mdiaOri := box[0]
{ // mdhd
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()})
if err != nil {
return err
}
oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()})
if err != nil {
return err
}
mdhd := oriBox[0].Payload.(*mp4.Mdhd)
if mdhd.Version == 0 {
mdhd.DurationV0 = uint32(duration)
} else {
mdhd.DurationV1 = duration
}
_, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // hdlr
oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()})
if err != nil {
return err
}
err = w.CopyBox(info.r, oriBox[0])
if err != nil {
return err
}
}
{ // minf
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()})
if err != nil {
return err
}
box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()})
if err != nil {
return err
}
minfOri := box[0]
{ // smhd, dinf
boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{
{mp4.BoxTypeSmhd()},
{mp4.BoxTypeDinf()},
})
if err != nil {
return err
}
for _, b := range boxes {
err = w.CopyBox(info.r, b)
if err != nil {
return err
}
}
}
{ // stbl
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()})
if err != nil {
return err
}
{ // stsd
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()})
if err != nil {
return err
}
_, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context)
if err != nil {
return err
}
{ // alac
_, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()})
if err != nil {
return err
}
_, err = w.Write([]byte{
0, 0, 0, 0, 0, 0, 0, 1,
0, 0, 0, 0, 0, 0, 0, 0})
if err != nil {
return err
}
err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels))
if err != nil {
return err
}
err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth))
if err != nil {
return err
}
_, err = w.Write([]byte{0, 0})
if err != nil {
return err
}
err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate)
if err != nil {
return err
}
_, err = w.Write([]byte{0, 0})
if err != nil {
return err
}
box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()})
if err != nil {
return err
}
_, err = mp4.Marshal(w, info.alacParam, box.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // stts
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()})
if err != nil {
return err
}
var stts mp4.Stts
for _, sample := range info.samples {
if len(stts.Entries) != 0 {
last := &stts.Entries[len(stts.Entries)-1]
if last.SampleDelta == sample.duration {
last.SampleCount++
continue
}
}
stts.Entries = append(stts.Entries, mp4.SttsEntry{
SampleCount: 1,
SampleDelta: sample.duration,
})
}
stts.EntryCount = uint32(len(stts.Entries))
_, err = mp4.Marshal(w, &stts, box.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // stsc
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()})
if err != nil {
return err
}
if numSamples%chunkSize == 0 {
_, err = mp4.Marshal(w, &mp4.Stsc{
EntryCount: 1,
Entries: []mp4.StscEntry{
{
FirstChunk: 1,
SamplesPerChunk: chunkSize,
SampleDescriptionIndex: 1,
},
},
}, box.Context)
} else {
_, err = mp4.Marshal(w, &mp4.Stsc{
EntryCount: 2,
Entries: []mp4.StscEntry{
{
FirstChunk: 1,
SamplesPerChunk: chunkSize,
SampleDescriptionIndex: 1,
}, {
FirstChunk: numSamples/chunkSize + 1,
SamplesPerChunk: numSamples % chunkSize,
SampleDescriptionIndex: 1,
},
},
}, box.Context)
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // stsz
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()})
if err != nil {
return err
}
stsz := mp4.Stsz{SampleCount: numSamples}
for _, sample := range info.samples {
stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data)))
}
_, err = mp4.Marshal(w, &stsz, box.Context)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // stco
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()})
if err != nil {
return err
}
l := (numSamples + chunkSize - 1) / chunkSize
_, err = mp4.Marshal(w, &mp4.Stco{
EntryCount: l,
ChunkOffset: make([]uint32, l),
}, box.Context)
stco, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // udta
ctx := mp4.Context{UnderUdta: true}
_, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx})
if err != nil {
return err
}
{ // meta
ctx.UnderIlstMeta = true
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx})
if err != nil {
return err
}
_, err = mp4.Marshal(w, &mp4.Meta{}, ctx)
if err != nil {
return err
}
{ // hdlr
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx})
if err != nil {
return err
}
_, err = mp4.Marshal(w, &mp4.Hdlr{
HandlerType: [4]byte{'m', 'd', 'i', 'r'},
Reserved: [3]uint32{0x6170706c, 0, 0},
}, ctx)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{ // ilst
ctx.UnderIlst = true
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx})
if err != nil {
return err
}
marshalData := func(val interface{}) error {
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()})
if err != nil {
return err
}
var boxData mp4.Data
switch v := val.(type) {
case string:
boxData.DataType = mp4.DataTypeStringUTF8
boxData.Data = []byte(v)
case uint8:
boxData.DataType = mp4.DataTypeSignedIntBigEndian
boxData.Data = []byte{v}
case uint32:
boxData.DataType = mp4.DataTypeSignedIntBigEndian
boxData.Data = make([]byte, 4)
binary.BigEndian.PutUint32(boxData.Data, v)
case []byte:
boxData.DataType = mp4.DataTypeBinary
boxData.Data = v
default:
panic("unsupported value")
}
_, err = mp4.Marshal(w, &boxData, ctx)
if err != nil {
return err
}
_, err = w.EndBox()
return err
}
addMeta := func(tag mp4.BoxType, val interface{}) error {
_, err = w.StartBox(&mp4.BoxInfo{Type: tag})
if err != nil {
return err
}
err = marshalData(val)
if err != nil {
return err
}
_, err = w.EndBox()
return err
}
addExtendedMeta := func(name string, val interface{}) error {
ctx.UnderIlstFreeMeta = true
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx})
if err != nil {
return err
}
{
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx})
if err != nil {
return err
}
_, err = w.Write([]byte{0, 0, 0, 0})
if err != nil {
return err
}
_, err = io.WriteString(w, "com.apple.iTunes")
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{
_, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx})
if err != nil {
return err
}
_, err = w.Write([]byte{0, 0, 0, 0})
if err != nil {
return err
}
_, err = io.WriteString(w, name)
if err != nil {
return err
}
_, err = w.EndBox()
if err != nil {
return err
}
}
err = marshalData(val)
if err != nil {
return err
}
ctx.UnderIlstFreeMeta = false
_, err = w.EndBox()
return err
}
err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'s', 'o', 'n', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name)
if err != nil {
return err
}
AlbumName := meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName
if strings.Contains(meta.Data[0].ID, "pl.") {
if !config.UseSongInfoForPlaylist {
AlbumName = meta.Data[0].Attributes.Name
}
}
err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'s', 'o', 'a', 'l'}, AlbumName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'\251', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'s', 'o', 'a', 'r'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'\251', 'p', 'r', 'f'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName)
if err != nil {
return err
}
err = addExtendedMeta("PERFORMER", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName)
if err != nil {
return err
}
err = addExtendedMeta("ITUNESALBUMID", meta.Data[0].ID)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'s', 'o', 'c', 'o'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, meta.Data[0].Attributes.ReleaseDate)
if err != nil {
return err
}
err = addExtendedMeta("RELEASETIME", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ReleaseDate)
if err != nil {
return err
}
cnID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].ID, 10, 32)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'c', 'n', 'I', 'D'}, uint32(cnID))
if err != nil {
return err
}
err = addExtendedMeta("ISRC", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc)
if err != nil {
return err
}
if len(meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames) > 0 {
err = addMeta(mp4.BoxType{'\251', 'g', 'e', 'n'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0])
if err != nil {
return err
}
}
if len(meta.Data) > 0 {
album := meta.Data[0]
err = addMeta(mp4.BoxType{'a', 'A', 'R', 'T'}, meta.Data[0].Attributes.ArtistName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'s', 'o', 'a', 'a'}, meta.Data[0].Attributes.ArtistName)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'c', 'p', 'r', 't'}, album.Attributes.Copyright)
if err != nil {
return err
}
var isCpil uint8
if album.Attributes.IsCompilation {
isCpil = 1
}
err = addMeta(mp4.BoxType{'c', 'p', 'i', 'l'}, isCpil)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'\251', 'p', 'u', 'b'}, album.Attributes.RecordLabel)
if err != nil {
return err
}
err = addExtendedMeta("LABEL", album.Attributes.RecordLabel)
if err != nil {
return err
}
err = addExtendedMeta("UPC", album.Attributes.Upc)
if err != nil {
return err
}
if !strings.Contains(meta.Data[0].ID, "pl.") {
plID, err := strconv.ParseUint(meta.Data[0].ID, 10, 32)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID))
if err != nil {
return err
}
}
}
if len(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data) > 0 {
if len(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data[0].ID) > 0 {
atID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].Relationships.Artists.Data[0].ID, 10, 32)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'a', 't', 'I', 'D'}, uint32(atID))
if err != nil {
return err
}
}
}
trkn := make([]byte, 8)
disk := make([]byte, 8)
binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber))
binary.BigEndian.PutUint16(disk[4:], uint16(meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber))
if strings.Contains(meta.Data[0].ID, "pl.") {
if !config.UseSongInfoForPlaylist {
binary.BigEndian.PutUint32(trkn, uint32(trackNum))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
binary.BigEndian.PutUint32(disk, uint32(1))
binary.BigEndian.PutUint16(disk[4:], uint16(1))
}
}
err = addMeta(mp4.BoxType{'t', 'r', 'k', 'n'}, trkn)
if err != nil {
return err
}
err = addMeta(mp4.BoxType{'d', 'i', 's', 'k'}, disk)
if err != nil {
return err
}
ctx.UnderIlst = false
_, err = w.EndBox()
if err != nil {
return err
}
}
ctx.UnderIlstMeta = false
_, err = w.EndBox()
if err != nil {
return err
}
}
ctx.UnderUdta = false
_, err = w.EndBox()
if err != nil {
return err
}
}
_, err = w.EndBox()
if err != nil {
return err
}
}
{
box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdat()})
if err != nil {
return err
}
_, err = mp4.Marshal(w, &mp4.Mdat{Data: data}, box.Context)
if err != nil {
return err
}
mdat, err := w.EndBox()
var realStco mp4.Stco
offset := mdat.Offset + mdat.HeaderSize
for i := uint32(0); i < numSamples; i++ {
if i%chunkSize == 0 {
realStco.EntryCount++
realStco.ChunkOffset = append(realStco.ChunkOffset, uint32(offset))
}
offset += uint64(len(info.samples[i].data))
}
_, err = stco.SeekToPayload(w)
if err != nil {
return err
}
_, err = mp4.Marshal(w, &realStco, box.Context)
if err != nil {
return err
}
}
return nil
}
func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filename string, trackNum, trackTotal int) error {
//fmt.Printf("%d-bit / %d Hz\n", info.bitDepth, info.bitRate)
conn, err := net.Dial("tcp", config.DecryptM3u8Port)
if err != nil {
return err
}
defer conn.Close()
var decrypted []byte
var lastIndex uint32 = math.MaxUint8
fmt.Println("Decrypting...")
bar := progressbar.NewOptions64(info.totalDataSize,
progressbar.OptionClearOnFinish(),
progressbar.OptionSetElapsedTime(false),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowElapsedTimeOnFinish(),
progressbar.OptionShowCount(),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
//progressbar.OptionSetDescription("Decrypting..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "",
SaucerHead: "",
SaucerPadding: "",
BarStart: "",
BarEnd: "",
}),
)
for _, sp := range info.samples {
if lastIndex != sp.descIndex {
if len(decrypted) != 0 {
_, err := conn.Write([]byte{0, 0, 0, 0})
if err != nil {
return err
}
}
keyUri := keys[sp.descIndex]
id := manifest.Data[0].Relationships.Tracks.Data[trackNum-1].ID
if keyUri == prefetchKey {
id = defaultId
}
_, err := conn.Write([]byte{byte(len(id))})
if err != nil {
return err
}
_, err = io.WriteString(conn, id)
if err != nil {
return err
}
_, err = conn.Write([]byte{byte(len(keyUri))})
if err != nil {
return err
}
_, err = io.WriteString(conn, keyUri)
if err != nil {
return err
}
}
lastIndex = sp.descIndex
err := binary.Write(conn, binary.LittleEndian, uint32(len(sp.data)))
if err != nil {
return err
}
_, err = conn.Write(sp.data)
if err != nil {
return err
}
de := make([]byte, len(sp.data))
_, err = io.ReadFull(conn, de)
if err != nil {
return err
}
decrypted = append(decrypted, de...)
bar.Add(len(sp.data))
}
_, _ = conn.Write([]byte{0, 0, 0, 0, 0})
fmt.Println("Decrypted.")
create, err := os.Create(filename)
if err != nil {
return err
}
defer create.Close()
if dl_atmos {
_, err = create.Write(decrypted)
if err != nil {
panic(err)
}
return nil
}
return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal)
}
func checkUrl(url string) (string, string) {
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`)
matches := pat.FindAllStringSubmatch(url, -1)
if matches == nil {
return "", ""
} else {
return matches[0][1], matches[0][2]
}
}
func checkUrlPlaylist(url string) (string, string) {
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?)`)
matches := pat.FindAllStringSubmatch(url, -1)
if matches == nil {
return "", ""
} else {
return matches[0][1], matches[0][2]
}
}
func checkUrlArtist(url string) (string, string) {
pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/artist|\/artist\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`)
matches := pat.FindAllStringSubmatch(url, -1)
if matches == nil {
return "", ""
} else {
return matches[0][1], matches[0][2]
}
}
func getUrlArtistName(artistUrl string, token string) (string, error) {
storefront, artistId := checkUrlArtist(artistUrl)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s", storefront, artistId), nil)
if err != nil {
return "", err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return "", errors.New(do.Status)
}
obj := new(AutoGeneratedArtist)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return "", err
}
return obj.Data[0].Attributes.Name, nil
}
func checkArtist(artistUrl string, token string) ([]string, error) {
storefront, artistId := checkUrlArtist(artistUrl)
Num := 0
var args []string
var urls []string
var options []string
for {
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d", storefront, artistId, Num), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(AutoGeneratedArtist)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
for _, album := range obj.Data {
urls = append(urls, album.Attributes.URL)
options = append(options, fmt.Sprintf("%s(%s)", album.Attributes.Name, album.ID))
}
Num = Num + 100
if len(obj.Next) == 0 {
break
}
}
for i, option := range options {
fmt.Printf("%02d: %s\n", i+1, option)
}
if artist_select {
fmt.Println("You have selected all options:")
return urls, nil
}
reader := bufio.NewReader(os.Stdin)
fmt.Println("Please select from the following options (multiple options separated by commas, ranges supported, or type 'all' to select all)")
fmt.Print("Enter your choice: ")
input, _ := reader.ReadString('\n')
// Remove newline and whitespace
input = strings.TrimSpace(input)
if input == "all" {
fmt.Println("You have selected all options:")
return urls, nil
}
// Split input into string slices
selectedOptions := [][]string{}
parts := strings.Split(input, ",")
for _, part := range parts {
if strings.Contains(part, "-") { // Range setting
rangeParts := strings.Split(part, "-")
selectedOptions = append(selectedOptions, rangeParts)
} else { // Single option
selectedOptions = append(selectedOptions, []string{part})
}
}
// Print selected options
fmt.Println("You have selected the following options:")
for _, opt := range selectedOptions {
if len(opt) == 1 { // Single option
num, err := strconv.Atoi(opt[0])
if err != nil {
fmt.Println("Invalid option:", opt[0])
continue
}
if num > 0 && num <= len(options) {
fmt.Println(options[num-1])
args = append(args, urls[num-1])
} else {
fmt.Println("Option out of range:", opt[0])
}
} else if len(opt) == 2 { // Range
start, err1 := strconv.Atoi(opt[0])
end, err2 := strconv.Atoi(opt[1])
if err1 != nil || err2 != nil {
fmt.Println("Invalid range:", opt)
continue
}
if start < 1 || end > len(options) || start > end {
fmt.Println("Range out of range:", opt)
continue
}
for i := start; i <= end; i++ {
fmt.Println(options[i-1])
args = append(args, urls[i-1])
}
} else {
fmt.Println("Invalid option:", opt)
}
}
return args, nil
}
func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) {
var mtype string
var page int
if strings.Contains(albumId, "pl.") {
mtype = "playlists"
} else {
mtype = "albums"
}
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
query := url.Values{}
query.Set("omit[resource]", "autos")
query.Set("include", "tracks,artists,record-labels")
query.Set("include[songs]", "artists")
query.Set("fields[artists]", "name,artwork")
query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url")
query.Set("fields[record-labels]", "name")
query.Set("extend", "editorialVideo")
// query.Set("l", "en-gb")
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(AutoGenerated)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
if strings.Contains(albumId, "pl.") {
obj.Data[0].Attributes.ArtistName = "Apple Music"
if len(obj.Data[0].Relationships.Tracks.Next) > 0 {
page = 0
for {
page = page + 100
pageStr := strconv.Itoa(page)
req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
req.Header.Set("Origin", "https://music.apple.com")
do, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj2 := new(AutoGeneratedTrack)
err = json.NewDecoder(do.Body).Decode(&obj2)
if err != nil {
return nil, err
}
for _, value := range obj2.Data {
obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value)
}
if len(obj2.Next) == 0 {
break
}
}
}
}
return obj, nil
}
func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) {
req, err := http.NewRequest("GET",
fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/%s", storefront, songId, config.LrcFormat), nil)
if err != nil {
return "", err
}
req.Header.Set("Origin", "https://music.apple.com")
req.Header.Set("Referer", "https://music.apple.com/")
req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
cookie := http.Cookie{Name: "media-user-token", Value: userToken}
req.AddCookie(&cookie)
do, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer do.Body.Close()
obj := new(SongLyrics)
err = json.NewDecoder(do.Body).Decode(&obj)
if obj.Data != nil {
return obj.Data[0].Attributes.Ttml, nil
} else {
return "", errors.New("failed to get lyrics")
}
}
func writeCover(sanAlbumFolder, name string, url string) error {
covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat)
if config.CoverFormat == "original" {
ext := strings.Split(url, "/")[len(strings.Split(url, "/"))-2]
ext = ext[strings.LastIndex(ext, ".")+1:]
covPath = filepath.Join(sanAlbumFolder, name+"."+ext)
}
exists, err := fileExists(covPath)
if err != nil {
fmt.Println("Failed to check if cover exists.")
return err
}
if exists {
return nil
}
if config.CoverFormat == "png" {
re := regexp.MustCompile(`\{w\}x\{h\}`)
parts := re.Split(url, 2)
url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1)
}
url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1)
if config.CoverFormat == "original" {
url = strings.Replace(url, "is1-ssl.mzstatic.com/image/thumb", "a5.mzstatic.com/us/r1000/0", 1)
url = url[:strings.LastIndex(url, "/")]
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return err
}
req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36")
do, err := http.DefaultClient.Do(req)
if err != nil {
return err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
errors.New(do.Status)
}
f, err := os.Create(covPath)
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, do.Body)
if err != nil {
return err
}
return nil
}
func writeLyrics(sanAlbumFolder, filename string, lrc string) error {
lyricspath := filepath.Join(sanAlbumFolder, filename)
f, err := os.Create(lyricspath)
if err != nil {
return err
}
defer f.Close()
_, err = f.WriteString(lrc)
if err != nil {
return err
}
return nil
}
func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
return true
}
}
return false
}
func rip(albumId string, token string, storefront string, userToken string) error {
var Codec string
if dl_atmos {
Codec = "Atmos"
} else {
Codec = "ALAC"
}
meta, err := getMeta(albumId, token, storefront)
if err != nil {
fmt.Println("Failed to get album metadata.\n")
return err
}
var singerFoldername string
if config.ArtistFolderFormat != "" {
if strings.Contains(albumId, "pl.") {
singerFoldername = strings.NewReplacer(
"{ArtistName}", "Apple Music",
"{ArtistId}", "",
"{UrlArtistName}", "Apple Music",
).Replace(config.ArtistFolderFormat)
} else if len(meta.Data[0].Relationships.Artists.Data) > 0 {
singerFoldername = strings.NewReplacer(
"{UrlArtistName}", LimitString(meta.Data[0].Attributes.ArtistName),
"{ArtistName}", LimitString(meta.Data[0].Attributes.ArtistName),
"{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID,
).Replace(config.ArtistFolderFormat)
} else {
singerFoldername = strings.NewReplacer(
"{UrlArtistName}", LimitString(meta.Data[0].Attributes.ArtistName),
"{ArtistName}", LimitString(meta.Data[0].Attributes.ArtistName),
"{ArtistId}", "",
).Replace(config.ArtistFolderFormat)
}
if strings.HasSuffix(singerFoldername, ".") {
singerFoldername = strings.ReplaceAll(singerFoldername, ".", "")
}
singerFoldername = strings.TrimSpace(singerFoldername)
fmt.Println(singerFoldername)
}
singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
if dl_atmos {
singerFolder = filepath.Join(config.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_"))
}
var Quality string
if strings.Contains(config.AlbumFolderFormat, "Quality") {
if dl_atmos {
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000)
} else {
manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront)
if err != nil {
fmt.Println("Failed to get manifest.\n", err)
} else {
if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("Unavailable.\n")
} else {
needCheck := false
if config.GetM3u8Mode == "all" {
needCheck = true
} else if config.GetM3u8Mode == "hires" && contains(meta.Data[0].Relationships.Tracks.Data[0].Attributes.AudioTraits, "hi-res-lossless") {
needCheck = true
}
var EnhancedHls_m3u8 string
if needCheck {
EnhancedHls_m3u8, err = checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album")
if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") {
manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
}
}
Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
}
}
}
}
}
stringsToJoin := []string{}
if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes {
if config.AppleMasterChoice != "" {
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
}
}
if meta.Data[0].Attributes.ContentRating == "explicit" {
if config.ExplicitChoice != "" {
stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
}
}
if meta.Data[0].Attributes.ContentRating == "clean" {
if config.CleanChoice != "" {
stringsToJoin = append(stringsToJoin, config.CleanChoice)
}
}
Tag_string := strings.Join(stringsToJoin, " ")
var albumFolder string
if strings.Contains(albumId, "pl.") {
albumFolder = strings.NewReplacer(
"{ArtistName}", "Apple Music",
"{PlaylistName}", LimitString(meta.Data[0].Attributes.Name),
"{PlaylistId}", albumId,
"{Quality}", Quality,
"{Codec}", Codec,
"{Tag}", Tag_string,
).Replace(config.PlaylistFolderFormat)
} else {
albumFolder = strings.NewReplacer(
"{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate,
"{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4],
"{ArtistName}", LimitString(meta.Data[0].Attributes.ArtistName),
"{AlbumName}", LimitString(meta.Data[0].Attributes.Name),
"{UPC}", meta.Data[0].Attributes.Upc,
"{RecordLabel}", meta.Data[0].Attributes.RecordLabel,
"{Copyright}", meta.Data[0].Attributes.Copyright,
"{AlbumId}", albumId,
"{Quality}", Quality,
"{Codec}", Codec,
"{Tag}", Tag_string,
).Replace(config.AlbumFolderFormat)
}
if strings.HasSuffix(albumFolder, ".") {
albumFolder = strings.ReplaceAll(albumFolder, ".", "")
}
albumFolder = strings.TrimSpace(albumFolder)
sanAlbumFolder := filepath.Join(singerFolder, forbiddenNames.ReplaceAllString(albumFolder, "_"))
os.MkdirAll(sanAlbumFolder, os.ModePerm)
fmt.Println(albumFolder)
//get artist cover
if config.SaveArtistCover && !(strings.Contains(albumId, "pl.")) {
if len(meta.Data[0].Relationships.Artists.Data) > 0 {
err = writeCover(singerFolder, "folder", meta.Data[0].Relationships.Artists.Data[0].Attributes.Artwork.Url)
if err != nil {
fmt.Println("Failed to write artist cover.")
}
}
}
//get album cover
err = writeCover(sanAlbumFolder, "cover", meta.Data[0].Attributes.Artwork.URL)
if err != nil {
fmt.Println("Failed to write cover.")
}
//get animated artwork
if config.SaveAnimatedArtwork && meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video != "" {
fmt.Println("Found Animation Artwork.")
motionvideoUrl, err := extractVideo(meta.Data[0].Attributes.EditorialVideo.MotionDetailSquare.Video)
if err != nil {
fmt.Println("no motion video.\n", err)
}
exists, err := fileExists(filepath.Join(sanAlbumFolder, "animated_artwork.mp4"))
if err != nil {
fmt.Println("Failed to check if animated artwork exists.")
}
if exists {
fmt.Println("Animated artwork already exists locally.")
} else {
fmt.Println("Animation Artwork Downloading...")
cmd := exec.Command("ffmpeg", "-loglevel", "quiet", "-y", "-i", motionvideoUrl, "-c", "copy", filepath.Join(sanAlbumFolder, "animated_artwork.mp4"))
if err := cmd.Run(); err != nil {
fmt.Printf("animated artwork dl err: %v\n", err)
} else {
fmt.Println("Animation Artwork Downloaded")
}
if config.EmbyAnimatedArtwork {
cmd2 := exec.Command("ffmpeg", "-i", filepath.Join(sanAlbumFolder, "animated_artwork.mp4"), "-vf", "scale=440:-1", "-r", "24", "-f", "gif", filepath.Join(sanAlbumFolder, "folder.jpg"))
if err := cmd2.Run(); err != nil {
fmt.Printf("animated artwork to gif err: %v\n", err)
}
}
}
}
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
arr := make([]int, trackTotal)
for i := 0; i < trackTotal; i++ {
arr[i] = i + 1
}
selected := []int{}
if !dl_select {
selected = arr
} else {
fmt.Print("select: ")
reader := bufio.NewReader(os.Stdin)
input, err := reader.ReadString('\n')
if err != nil {
fmt.Println(err)
}
input = strings.TrimSpace(input)
inputs := strings.Fields(input)
for _, str := range inputs {
num, err := strconv.Atoi(str)
if err != nil {
fmt.Printf("wrong '%s', skip...\n", str)
continue
}
found := false
for i := 0; i < len(arr); i++ {
if arr[i] == num {
selected = append(selected, num)
found = true
break
}
}
if !found {
fmt.Printf("Option '%d' not found or already selected, skipping...\n", num)
}
}
fmt.Println("Selected options:", selected)
}
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
if isInArray(okDict[albumId], trackNum) {
//fmt.Println("已完成直接跳过.\n")
counter.Total++
counter.Success++
continue
}
if isInArray(selected, trackNum) {
counter.Total++
fmt.Printf("Track %d of %d:\n", trackNum, trackTotal)
manifest, err := getInfoFromAdam(track.ID, token, storefront)
if err != nil {
fmt.Println("\u26A0 Failed to get manifest:", err)
counter.NotSong++
continue
}
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" {
fmt.Println("\u26A0 Unavailable.")
counter.Unavailable++
continue
}
needCheck := false
if config.GetM3u8Mode == "all" {
needCheck = true
} else if config.GetM3u8Mode == "hires" && contains(track.Attributes.AudioTraits, "hi-res-lossless") {
needCheck = true
}
var EnhancedHls_m3u8 string
if needCheck {
EnhancedHls_m3u8, err = checkM3u8(track.ID, "song")
if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") {
manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8
}
}
var Quality string
if strings.Contains(config.SongFileFormat, "Quality") {
if dl_atmos {
Quality = fmt.Sprintf("%dkbps", config.AtmosMax-2000)
} else {
Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("Failed to extract quality from manifest.\n", err)
counter.Error++
continue
}
}
}
stringsToJoin := []string{}
if track.Attributes.IsAppleDigitalMaster {
if config.AppleMasterChoice != "" {
stringsToJoin = append(stringsToJoin, config.AppleMasterChoice)
}
}
if track.Attributes.ContentRating == "explicit" {
if config.ExplicitChoice != "" {
stringsToJoin = append(stringsToJoin, config.ExplicitChoice)
}
}
if track.Attributes.ContentRating == "clean" {
if config.CleanChoice != "" {
stringsToJoin = append(stringsToJoin, config.CleanChoice)
}
}
Tag_string := strings.Join(stringsToJoin, " ")
songName := strings.NewReplacer(
"{SongId}", track.ID,
"{SongNumer}", fmt.Sprintf("%02d", trackNum),
"{SongName}", LimitString(track.Attributes.Name),
"{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber),
"{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber),
"{Quality}", Quality,
"{Tag}", Tag_string,
"{Codec}", Codec,
).Replace(config.SongFileFormat)
fmt.Println(songName)
filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
if dl_atmos {
filename = fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_"))
}
m4afilename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_"))
lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_"))
trackPath := filepath.Join(sanAlbumFolder, filename)
m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename)
var lrc string = ""
if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) {
ttml, err := getSongLyrics(track.ID, storefront, token, userToken)
if err != nil {
fmt.Println("Failed to get lyrics")
} else {
lrc, err = conventTTMLToLRC(ttml)
if err != nil {
fmt.Printf("Failed to parse lyrics: %s \n", err)
} else {
if config.SaveLrcFile {
err := writeLyrics(sanAlbumFolder, lrcFilename, lrc)
if err != nil {
fmt.Printf("Failed to write lyrics")
}
if !config.EmbedLrc {
lrc = ""
}
}
}
}
}
exists, err := fileExists(trackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if exists {
fmt.Println("Track already exists locally.")
counter.Success++
okDict[albumId] = append(okDict[albumId], trackNum)
continue
}
m4aexists, err := fileExists(m4atrackPath)
if err != nil {
fmt.Println("Failed to check if track exists.")
}
if m4aexists {
fmt.Println("Track already exists locally.")
counter.Success++
okDict[albumId] = append(okDict[albumId], trackNum)
continue
}
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println("\u26A0 Failed to extract info from manifest:", err)
counter.Unavailable++
continue
}
info, err := extractSong(trackUrl)
if err != nil {
fmt.Println("Failed to extract track.", err)
counter.Error++
continue
}
samplesOk := true
for samplesOk {
var totalSize int64 = 0
for _, i := range info.samples {
totalSize += int64(len(i.data))
if int(i.descIndex) >= len(keys) {
fmt.Println("Decryption size mismatch.")
samplesOk = false
}
}
info.totalDataSize = totalSize
break
}
if !samplesOk {
counter.Error++
continue
}
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
if err != nil {
fmt.Println("Failed to decrypt track.\n", err)
counter.Error++
continue
}
tags := []string{
fmt.Sprintf("lyrics=%s", lrc),
}
index := trackNum - 1
if dl_atmos {
tags = []string{
"tool=",
fmt.Sprintf("lyrics=%s", lrc),
fmt.Sprintf("title=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name),
fmt.Sprintf("artist=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName),
fmt.Sprintf("genre=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]),
fmt.Sprintf("created=%s", meta.Data[0].Attributes.ReleaseDate),
fmt.Sprintf("album_artist=%s", meta.Data[0].Attributes.ArtistName),
fmt.Sprintf("composer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName),
fmt.Sprintf("writer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName),
fmt.Sprintf("performer=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName),
fmt.Sprintf("copyright=%s", meta.Data[0].Attributes.Copyright),
fmt.Sprintf("ISRC=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc),
fmt.Sprintf("UPC=%s", meta.Data[0].Attributes.Upc),
}
if strings.Contains(albumId, "pl.") && !config.UseSongInfoForPlaylist {
tags = append(tags, "disk=1/1")
tags = append(tags, fmt.Sprintf("track=%d", trackNum))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", trackNum, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Attributes.Name))
} else {
tags = append(tags, fmt.Sprintf("disk=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber, meta.Data[0].Relationships.Tracks.Data[trackTotal-1].Attributes.DiscNumber))
tags = append(tags, fmt.Sprintf("track=%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
tags = append(tags, fmt.Sprintf("tracknum=%d/%d", meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber, trackTotal))
tags = append(tags, fmt.Sprintf("album=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName))
}
}
if track.Attributes.ContentRating == "explicit" {
tags = append(tags, "rating=1")
} else if track.Attributes.ContentRating == "clean" {
tags = append(tags, "rating=2")
} else {
tags = append(tags, "rating=0")
}
if config.EmbedCover {
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL)
if err != nil {
fmt.Println("Failed to write cover.")
}
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat))
} else {
tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat))
}
}
tagsString := strings.Join(tags, ":")
cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath)
if dl_atmos {
cmd = exec.Command("MP4Box", "-add", trackPath, "-name", fmt.Sprintf("1=%s", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name), "-itags", tagsString, "-brand", "mp42", "-ab", "dby1", m4atrackPath)
}
if err := cmd.Run(); err != nil {
fmt.Printf("Embed failed: %v\n", err)
counter.Error++
continue
}
if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist {
if err := os.Remove(fmt.Sprintf("%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)); err != nil {
fmt.Printf("Error deleting file: %s/%s.%s\n", sanAlbumFolder, track.ID, config.CoverFormat)
counter.Error++
continue
}
}
if dl_atmos {
fmt.Printf("Deleting original EC3 file: %s\n", filepath.Base(trackPath))
if err := os.Remove(trackPath); err != nil {
fmt.Printf("Error deleting file: %v\n", err)
counter.Error++
continue
}
fmt.Printf("Successfully processed and deleted %s\n", filepath.Base(trackPath))
}
counter.Success++
okDict[albumId] = append(okDict[albumId], trackNum)
}
}
return err
}
func main() {
err := loadConfig()
if err != nil {
fmt.Printf("load config failed: %v", err)
return
}
token, err := getToken()
if err != nil {
if config.AuthorizationToken != "" && config.AuthorizationToken != "your-authorization-token" {
token = strings.Replace(config.AuthorizationToken, "Bearer ", "", -1)
} else {
fmt.Println("Failed to get token.")
return
}
}
// Define command-line flags
pflag.BoolVar(&dl_atmos, "atmos", false, "Enable atmos download mode")
pflag.BoolVar(&dl_select, "select", false, "Enable selective download")
pflag.BoolVar(&artist_select, "all-album", false, "Download all artist albums")
alac_max = pflag.Int("alac-max", -1, "Specify the max quality for download alac")
atmos_max = pflag.Int("atmos-max", -1, "Specify the max quality for download atmos")
// Custom usage message for help
pflag.Usage = func() {
fmt.Fprintf(os.Stderr, "Usage: %s [options] url1 url2 ...\n", "[main | main.exe | go run main.go]")
fmt.Println("Options:")
pflag.PrintDefaults()
}
// Parse the flag arguments
pflag.Parse()
if *alac_max != -1 {
config.AlacMax = *alac_max
}
if *atmos_max != -1 {
config.AtmosMax = *atmos_max
}
args := pflag.Args()
if len(args) == 0 {
fmt.Println("No URLs provided. Please provide at least one URL.")
pflag.Usage()
return
}
os.Args = args
if strings.Contains(os.Args[0], "/artist/") {
urlArtistName, err := getUrlArtistName(os.Args[0], token)
if err != nil {
fmt.Println("Failed to get artistname.")
return
}
//fmt.Println("get artistname:", urlArtistName)
config.ArtistFolderFormat = strings.NewReplacer(
"{UrlArtistName}", LimitString(urlArtistName),
).Replace(config.ArtistFolderFormat)
newArgs, err := checkArtist(os.Args[0], token)
if err != nil {
fmt.Println("Failed to get artist.")
return
}
os.Args = newArgs
}
albumTotal := len(os.Args)
for {
for albumNum, url := range os.Args {
fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal)
var storefront, albumId string
if strings.Contains(url, ".txt") {
txtpath = url
fileName := filepath.Base(url)
parts := strings.SplitN(fileName, "_", 3)
storefront = parts[0]
albumId = parts[1]
} else {
if strings.Contains(url, "/playlist/") {
storefront, albumId = checkUrlPlaylist(url)
txtpath = ""
} else {
storefront, albumId = checkUrl(url)
txtpath = ""
}
}
if albumId == "" {
fmt.Printf("Invalid URL: %s\n", url)
continue
}
err = rip(albumId, token, storefront, config.MediaUserToken)
if err != nil {
fmt.Println("Album failed.")
fmt.Println(err)
}
}
fmt.Printf("======= [\u2714 ] Completed: %d/%d | [\u26A0 ] Warnings: %d | [\u2716 ] Errors: %d =======\n", counter.Success, counter.Total, counter.Unavailable+counter.NotSong, counter.Error)
if counter.Error == 0 {
break
}
fmt.Println("Error detected, press Enter to try again...")
fmt.Scanln()
fmt.Println("Start trying again...")
counter = Counter{}
}
}
func conventSyllableTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return "", err
}
var lrcLines []string
parseTime := func(timeValue string) (string, error) {
var h, m, s, ms int
if strings.Contains(timeValue, ":") {
_, err = fmt.Sscanf(timeValue, "%d:%d:%d.%d", &h, &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(timeValue, "%d:%d.%d", &m, &s, &ms)
h = 0
}
} else {
_, err = fmt.Sscanf(timeValue, "%d.%d", &s, &ms)
h, m = 0, 0
}
if err != nil {
return "", err
}
m += h * 60
ms = ms / 10
return fmt.Sprintf("[%02d:%02d.%02d]", m, s, ms), nil
}
for _, div := range parsedTTML.FindElement("tt").FindElement("body").FindElements("div") {
for _, item := range div.ChildElements() {
var lrcSyllables []string
var i int = 0
for _, lyrics := range item.Child {
if _, ok := lyrics.(*etree.CharData); ok {
if i > 0 {
lrcSyllables = append(lrcSyllables, " ")
continue
}
continue
}
lyric := lyrics.(*etree.Element)
if lyric.SelectAttr("begin") == nil {
continue
}
beginTime, err := parseTime(lyric.SelectAttr("begin").Value)
if err != nil {
return "", err
}
var text string
if lyric.SelectAttr("text") == nil {
var textTmp []string
for _, span := range lyric.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}
}
text = strings.Join(textTmp, "")
} else {
text = lyric.SelectAttr("text").Value
}
lrcSyllables = append(lrcSyllables, fmt.Sprintf("%s%s", beginTime, text))
i += 1
}
endTime, err := parseTime(item.SelectAttr("end").Value)
if err != nil {
return "", err
}
lrcLines = append(lrcLines, strings.Join(lrcSyllables, "")+endTime)
}
}
return strings.Join(lrcLines, "\n"), nil
}
func conventTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return "", err
}
var lrcLines []string
timingAttr := parsedTTML.FindElement("tt").SelectAttr("itunes:timing")
if timingAttr != nil {
if timingAttr.Value == "Word" {
lrc, err := conventSyllableTTMLToLRC(ttml)
return lrc, err
}
if timingAttr.Value == "None" {
for _, p := range parsedTTML.FindElements("//p") {
line := p.Text()
line = strings.TrimSpace(line)
if line != "" {
lrcLines = append(lrcLines, line)
}
}
return strings.Join(lrcLines, "\n"), nil
}
}
for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() {
for _, lyric := range item.ChildElements() {
var h, m, s, ms int
if lyric.SelectAttr("begin") == nil {
return "", errors.New("no synchronised lyrics")
}
if strings.Contains(lyric.SelectAttr("begin").Value, ":") {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms)
if err != nil {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d", &m, &s)
}
h = 0
}
} else {
_, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms)
h, m = 0, 0
}
if err != nil {
return "", err
}
var text string
if lyric.SelectAttr("text") == nil {
var textTmp []string
for _, span := range lyric.Child {
if _, ok := span.(*etree.CharData); ok {
textTmp = append(textTmp, span.(*etree.CharData).Data)
} else {
textTmp = append(textTmp, span.(*etree.Element).Text())
}
}
text = strings.Join(textTmp, "")
} else {
text = lyric.SelectAttr("text").Value
}
m += h * 60
ms = ms / 10
lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text))
}
}
return strings.Join(lrcLines, "\n"), nil
}
func checkM3u8(b string, f string) (string, error) {
var EnhancedHls string
if config.Check != "" {
config.Check = strings.TrimSpace(config.Check)
if strings.HasSuffix(config.Check, "txt") {
txtpath = config.Check
}
if strings.HasPrefix(config.Check, "http") {
req, err := http.NewRequest("GET", config.Check, nil)
if err != nil {
fmt.Println(err)
}
query := req.URL.Query()
query.Set("songid", b)
req.URL.RawQuery = query.Encode()
do, err := http.DefaultClient.Do(req)
if err != nil {
fmt.Println(err)
}
defer do.Body.Close()
Checkbody, err := ioutil.ReadAll(do.Body)
if err != nil {
fmt.Println(err)
}
if string(Checkbody) != "no_found" {
EnhancedHls = string(Checkbody)
fmt.Println("Found m3u8 from API")
} else {
if config.ForceApi {
fmt.Println(" Not Found m3u8 from API, Skip")
}
fmt.Println(" Not Found m3u8 from API")
}
}
}
if config.GetM3u8FromDevice {
adamID := b
conn, err := net.Dial("tcp", config.GetM3u8Port)
if err != nil {
fmt.Println("Error connecting to device:", err)
return "none", err
}
defer conn.Close()
if f == "song" {
fmt.Println("Connected to device")
}
// Send the length of adamID and the adamID itself
adamIDBuffer := []byte(adamID)
lengthBuffer := []byte{byte(len(adamIDBuffer))}
// Write length and adamID to the connection
_, err = conn.Write(lengthBuffer)
if err != nil {
fmt.Println("Error writing length to device:", err)
return "none", err
}
_, err = conn.Write(adamIDBuffer)
if err != nil {
fmt.Println("Error writing adamID to device:", err)
return "none", err
}
// Read the response (URL) from the device
response, err := bufio.NewReader(conn).ReadBytes('\n')
if err != nil {
fmt.Println("Error reading response from device:", err)
return "none", err
}
// Trim any newline characters from the response
response = bytes.TrimSpace(response)
if len(response) > 0 {
if f == "song" {
fmt.Println("Received URL:", string(response))
}
EnhancedHls = string(response)
} else {
fmt.Println("Received an empty response")
}
}
if txtpath != "" {
file, err := os.Open(txtpath)
if err != nil {
fmt.Println("cant open txt:", err)
}
defer file.Close()
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := scanner.Text()
if strings.HasPrefix(line, b) {
parts := strings.SplitN(line, ",", 2)
if len(parts) == 2 {
EnhancedHls = parts[1]
fmt.Println("Found m3u8 from txt")
}
}
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
}
return EnhancedHls, nil
}
func extractMediaQuality(b string) (string, error) {
resp, err := http.Get(b)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
masterString := string(body)
from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true)
if err != nil || listType != m3u8.MASTER {
return "", errors.New("m3u8 not of master type")
}
master := from.(*m3u8.MasterPlaylist)
sort.Slice(master.Variants, func(i, j int) bool {
return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth
})
var Quality string
for _, variant := range master.Variants {
if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-2])
if err != nil {
return "", err
}
if length_int <= config.AlacMax {
HZ, err := strconv.Atoi(split[length-2])
if err != nil {
fmt.Println(err)
}
KHZ := float64(HZ) / 1000.0
Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ)
break
}
}
}
return Quality, nil
}
func extractMedia(b string) (string, []string, error) {
masterUrl, err := url.Parse(b)
if err != nil {
return "", nil, err
}
resp, err := http.Get(b)
if err != nil {
return "", nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", nil, errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", nil, err
}
masterString := string(body)
from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true)
if err != nil || listType != m3u8.MASTER {
return "", nil, errors.New("m3u8 not of master type")
}
master := from.(*m3u8.MasterPlaylist)
var streamUrl *url.URL
sort.Slice(master.Variants, func(i, j int) bool {
return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth
})
for _, variant := range master.Variants {
if dl_atmos {
if variant.Codecs == "ec-3" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-1])
if err != nil {
return "", nil, err
}
if length_int <= config.AtmosMax {
fmt.Printf("%s\n", variant.Audio)
streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil {
panic(err)
}
streamUrl = streamUrlTemp
break
}
}
} else {
if variant.Codecs == "alac" {
split := strings.Split(variant.Audio, "-")
length := len(split)
length_int, err := strconv.Atoi(split[length-2])
if err != nil {
return "", nil, err
}
if length_int <= config.AlacMax {
fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2])
streamUrlTemp, err := masterUrl.Parse(variant.URI)
if err != nil {
panic(err)
}
streamUrl = streamUrlTemp
break
}
}
}
}
if streamUrl == nil {
return "", nil, errors.New("no codec found")
}
var keys []string
keys = append(keys, prefetchKey)
streamUrl.Path = strings.TrimSuffix(streamUrl.Path, ".m3u8") + "_m.mp4"
regex := regexp.MustCompile(`"(skd?://[^"]*)"`)
matches := regex.FindAllStringSubmatch(masterString, -1)
for _, match := range matches {
if dl_atmos {
if strings.HasSuffix(match[1], "c24") || strings.HasSuffix(match[1], "c6") {
keys = append(keys, match[1])
}
} else {
if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") {
keys = append(keys, match[1])
}
}
}
return streamUrl.String(), keys, nil
}
func extractVideo(c string) (string, error) {
MediaUrl, err := url.Parse(c)
if err != nil {
return "", err
}
resp, err := http.Get(c)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return "", errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
videoString := string(body)
from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true)
if err != nil || listType != m3u8.MASTER {
return "", errors.New("m3u8 not of media type")
}
video := from.(*m3u8.MasterPlaylist)
var streamUrl *url.URL
sort.Slice(video.Variants, func(i, j int) bool {
return video.Variants[i].AverageBandwidth > video.Variants[j].AverageBandwidth
})
if len(video.Variants) > 0 {
highestBandwidthVariant := video.Variants[0]
streamUrl, err = MediaUrl.Parse(highestBandwidthVariant.URI)
if err != nil {
return "", err
}
}
if streamUrl == nil {
return "", errors.New("no video codec found")
}
return streamUrl.String(), nil
}
func extractSong(url string) (*SongInfo, error) {
fmt.Println("Downloading...")
track, err := http.Get(url)
if err != nil {
return nil, err
}
defer track.Body.Close()
if track.StatusCode != http.StatusOK {
return nil, errors.New(track.Status)
}
contentLength := track.ContentLength
bar := progressbar.NewOptions64(contentLength,
progressbar.OptionClearOnFinish(),
progressbar.OptionSetElapsedTime(false),
progressbar.OptionSetPredictTime(false),
progressbar.OptionShowElapsedTimeOnFinish(),
progressbar.OptionShowCount(),
progressbar.OptionEnableColorCodes(true),
progressbar.OptionShowBytes(true),
//progressbar.OptionSetDescription("Downloading..."),
progressbar.OptionSetTheme(progressbar.Theme{
Saucer: "",
SaucerHead: "",
SaucerPadding: "",
BarStart: "",
BarEnd: "",
}),
)
rawSong, err := ioutil.ReadAll(io.TeeReader(track.Body, bar))
if err != nil {
return nil, err
}
fmt.Println("Downloaded.")
f := bytes.NewReader(rawSong)
trex, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{
mp4.BoxTypeMoov(),
mp4.BoxTypeMvex(),
mp4.BoxTypeTrex(),
})
if err != nil || len(trex) != 1 {
return nil, err
}
trexPay := trex[0].Payload.(*mp4.Trex)
stbl, err := mp4.ExtractBox(f, nil, []mp4.BoxType{
mp4.BoxTypeMoov(),
mp4.BoxTypeTrak(),
mp4.BoxTypeMdia(),
mp4.BoxTypeMinf(),
mp4.BoxTypeStbl(),
})
if err != nil || len(stbl) != 1 {
return nil, err
}
var extracted *SongInfo
if !dl_atmos {
enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{
mp4.BoxTypeStsd(),
mp4.BoxTypeEnca(),
})
if err != nil {
return nil, err
}
aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info,
[]mp4.BoxType{BoxTypeAlac()})
if err != nil || len(aalac) != 1 {
return nil, err
}
extracted = &SongInfo{
r: f,
alacParam: aalac[0].Payload.(*Alac),
}
} else {
extracted = &SongInfo{
r: f,
// alacParam: aalac[0].Payload.(*Alac),
}
}
moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{
mp4.BoxTypeMoof(),
})
if err != nil || len(moofs) <= 0 {
return nil, err
}
mdats, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{
mp4.BoxTypeMdat(),
})
if err != nil || len(mdats) != len(moofs) {
return nil, err
}
for i, moof := range moofs {
tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{
mp4.BoxTypeTraf(),
mp4.BoxTypeTfhd(),
})
if err != nil || len(tfhd) != 1 {
return nil, err
}
tfhdPay := tfhd[0].Payload.(*mp4.Tfhd)
index := tfhdPay.SampleDescriptionIndex
if index != 0 {
index--
}
truns, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{
mp4.BoxTypeTraf(),
mp4.BoxTypeTrun(),
})
if err != nil || len(truns) <= 0 {
return nil, err
}
mdat := mdats[i].Payload.(*mp4.Mdat).Data
for _, t := range truns {
for _, en := range t.Payload.(*mp4.Trun).Entries {
info := SampleInfo{descIndex: index}
switch {
case t.Payload.CheckFlag(0x200):
info.data = mdat[:en.SampleSize]
mdat = mdat[en.SampleSize:]
case tfhdPay.CheckFlag(0x10):
info.data = mdat[:tfhdPay.DefaultSampleSize]
mdat = mdat[tfhdPay.DefaultSampleSize:]
default:
info.data = mdat[:trexPay.DefaultSampleSize]
mdat = mdat[trexPay.DefaultSampleSize:]
}
switch {
case t.Payload.CheckFlag(0x100):
info.duration = en.SampleDuration
case tfhdPay.CheckFlag(0x8):
info.duration = tfhdPay.DefaultSampleDuration
default:
info.duration = trexPay.DefaultSampleDuration
}
extracted.samples = append(extracted.samples, info)
}
}
if len(mdat) != 0 {
return nil, errors.New("offset mismatch")
}
}
return extracted, nil
}
func init() {
mp4.AddBoxDef((*Alac)(nil))
}
func BoxTypeAlac() mp4.BoxType { return mp4.StrToBoxType("alac") }
type Alac struct {
mp4.FullBox `mp4:"extend"`
FrameLength uint32 `mp4:"size=32"`
CompatibleVersion uint8 `mp4:"size=8"`
BitDepth uint8 `mp4:"size=8"`
Pb uint8 `mp4:"size=8"`
Mb uint8 `mp4:"size=8"`
Kb uint8 `mp4:"size=8"`
NumChannels uint8 `mp4:"size=8"`
MaxRun uint16 `mp4:"size=16"`
MaxFrameBytes uint32 `mp4:"size=32"`
AvgBitRate uint32 `mp4:"size=32"`
SampleRate uint32 `mp4:"size=32"`
}
func getInfoFromAdam(adamId string, token string, storefront string) (*SongData, error) {
request, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, adamId), nil)
if err != nil {
return nil, err
}
query := url.Values{}
query.Set("extend", "extendedAssetUrls")
query.Set("include", "albums")
request.URL.RawQuery = query.Encode()
request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token))
request.Header.Set("User-Agent", "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)")
request.Header.Set("Origin", "https://music.apple.com")
do, err := http.DefaultClient.Do(request)
if err != nil {
return nil, err
}
defer do.Body.Close()
if do.StatusCode != http.StatusOK {
return nil, errors.New(do.Status)
}
obj := new(ApiResult)
err = json.NewDecoder(do.Body).Decode(&obj)
if err != nil {
return nil, err
}
for _, d := range obj.Data {
if d.ID == adamId {
return &d, nil
}
}
return nil, nil
}
func getToken() (string, error) {
req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil)
if err != nil {
return "", err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`)
indexJsUri := regex.FindString(string(body))
req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil)
if err != nil {
return "", err
}
resp, err = http.DefaultClient.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
body, err = io.ReadAll(resp.Body)
if err != nil {
return "", err
}
regex = regexp.MustCompile(`eyJh([^"]*)`)
token := regex.FindString(string(body))
return token, nil
}
type ApiResult struct {
Data []SongData `json:"data"`
}
type SongAttributes struct {
ArtistName string `json:"artistName"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
ExtendedAssetUrls struct {
EnhancedHls string `json:"enhancedHls"`
} `json:"extendedAssetUrls"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AlbumName string `json:"albumName"`
TrackNumber int `json:"trackNumber"`
ComposerName string `json:"composerName"`
}
type AlbumAttributes struct {
ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"`
IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"`
Copyright string `json:"copyright"`
IsCompilation bool `json:"isCompilation"`
}
type SongData struct {
ID string `json:"id"`
Attributes SongAttributes `json:"attributes"`
Relationships struct {
Albums struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes AlbumAttributes `json:"attributes"`
} `json:"data"`
} `json:"albums"`
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
} `json:"data"`
} `json:"artists"`
} `json:"relationships"`
}
type SongResult struct {
Artwork struct {
Width int `json:"width"`
URL string `json:"url"`
Height int `json:"height"`
TextColor3 string `json:"textColor3"`
TextColor2 string `json:"textColor2"`
TextColor4 string `json:"textColor4"`
HasAlpha bool `json:"hasAlpha"`
TextColor1 string `json:"textColor1"`
BgColor string `json:"bgColor"`
HasP3 bool `json:"hasP3"`
SupportsLayeredImage bool `json:"supportsLayeredImage"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
CollectionID string `json:"collectionId"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
ID string `json:"id"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
ContentRatingsBySystem struct {
} `json:"contentRatingsBySystem"`
Name string `json:"name"`
Composer struct {
Name string `json:"name"`
URL string `json:"url"`
} `json:"composer"`
EditorialArtwork struct {
} `json:"editorialArtwork"`
CollectionName string `json:"collectionName"`
AssetUrls struct {
Plus string `json:"plus"`
Lightweight string `json:"lightweight"`
SuperLightweight string `json:"superLightweight"`
LightweightPlus string `json:"lightweightPlus"`
EnhancedHls string `json:"enhancedHls"`
} `json:"assetUrls"`
AudioTraits []string `json:"audioTraits"`
Kind string `json:"kind"`
Copyright string `json:"copyright"`
ArtistID string `json:"artistId"`
Genres []struct {
GenreID string `json:"genreId"`
Name string `json:"name"`
URL string `json:"url"`
MediaType string `json:"mediaType"`
} `json:"genres"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
Offers []struct {
ActionText struct {
Short string `json:"short"`
Medium string `json:"medium"`
Long string `json:"long"`
Downloaded string `json:"downloaded"`
Downloading string `json:"downloading"`
} `json:"actionText"`
Type string `json:"type"`
PriceFormatted string `json:"priceFormatted"`
Price float64 `json:"price"`
BuyParams string `json:"buyParams"`
Variant string `json:"variant,omitempty"`
Assets []struct {
Flavor string `json:"flavor"`
Preview struct {
Duration int `json:"duration"`
URL string `json:"url"`
} `json:"preview"`
Size int `json:"size"`
Duration int `json:"duration"`
} `json:"assets"`
} `json:"offers"`
}
type iTunesLookup struct {
Results map[string]SongResult `json:"results"`
}
type Meta struct {
Context string `json:"@context"`
Type string `json:"@type"`
Name string `json:"name"`
Description string `json:"description"`
Tracks []struct {
Type string `json:"@type"`
Name string `json:"name"`
Audio struct {
Type string `json:"@type"`
} `json:"audio"`
Offers struct {
Type string `json:"@type"`
Category string `json:"category"`
Price int `json:"price"`
} `json:"offers"`
Duration string `json:"duration"`
} `json:"tracks"`
Citation []interface{} `json:"citation"`
WorkExample []struct {
Type string `json:"@type"`
Name string `json:"name"`
URL string `json:"url"`
Audio struct {
Type string `json:"@type"`
} `json:"audio"`
Offers struct {
Type string `json:"@type"`
Category string `json:"category"`
Price int `json:"price"`
} `json:"offers"`
Duration string `json:"duration"`
} `json:"workExample"`
Genre []string `json:"genre"`
DatePublished time.Time `json:"datePublished"`
ByArtist struct {
Type string `json:"@type"`
URL string `json:"url"`
Name string `json:"name"`
} `json:"byArtist"`
}
type AutoGenerated struct {
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
IsSingle bool `json:"isSingle"`
URL string `json:"url"`
IsComplete bool `json:"isComplete"`
GenreNames []string `json:"genreNames"`
TrackCount int `json:"trackCount"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
RecordLabel string `json:"recordLabel"`
Upc string `json:"upc"`
AudioTraits []string `json:"audioTraits"`
Copyright string `json:"copyright"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
IsCompilation bool `json:"isCompilation"`
EditorialVideo struct {
MotionDetailSquare struct {
Video string `json:"video"`
} `json:"motionDetailSquare"`
MotionSquareVideo1x1 struct {
Video string `json:"video"`
} `json:"motionSquareVideo1x1"`
} `json:"editorialVideo"`
} `json:"attributes"`
Relationships struct {
RecordLabels struct {
Href string `json:"href"`
Data []interface{} `json:"data"`
} `json:"record-labels"`
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
Artwork struct {
Url string `json:"url"`
} `json:"artwork"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
Tracks struct {
Href string `json:"href"`
Next string `json:"next"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
ComposerName string `json:"composerName"`
} `json:"attributes"`
Relationships struct {
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
} `json:"relationships"`
} `json:"data"`
} `json:"tracks"`
} `json:"relationships"`
} `json:"data"`
}
type AutoGeneratedTrack struct {
Href string `json:"href"`
Next string `json:"next"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
ComposerName string `json:"composerName"`
} `json:"attributes"`
Relationships struct {
Artists struct {
Href string `json:"href"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Name string `json:"name"`
} `json:"attributes"`
} `json:"data"`
} `json:"artists"`
} `json:"relationships"`
} `json:"data"`
}
type AutoGeneratedArtist struct {
Next string `json:"next"`
Data []struct {
ID string `json:"id"`
Type string `json:"type"`
Href string `json:"href"`
Attributes struct {
Previews []struct {
URL string `json:"url"`
} `json:"previews"`
Artwork struct {
Width int `json:"width"`
Height int `json:"height"`
URL string `json:"url"`
BgColor string `json:"bgColor"`
TextColor1 string `json:"textColor1"`
TextColor2 string `json:"textColor2"`
TextColor3 string `json:"textColor3"`
TextColor4 string `json:"textColor4"`
} `json:"artwork"`
ArtistName string `json:"artistName"`
URL string `json:"url"`
DiscNumber int `json:"discNumber"`
GenreNames []string `json:"genreNames"`
HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"`
IsMasteredForItunes bool `json:"isMasteredForItunes"`
IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"`
ContentRating string `json:"contentRating"`
DurationInMillis int `json:"durationInMillis"`
ReleaseDate string `json:"releaseDate"`
Name string `json:"name"`
Isrc string `json:"isrc"`
AudioTraits []string `json:"audioTraits"`
HasLyrics bool `json:"hasLyrics"`
AlbumName string `json:"albumName"`
PlayParams struct {
ID string `json:"id"`
Kind string `json:"kind"`
} `json:"playParams"`
TrackNumber int `json:"trackNumber"`
AudioLocale string `json:"audioLocale"`
ComposerName string `json:"composerName"`
} `json:"attributes"`
} `json:"data"`
}
type SongLyrics struct {
Data []struct {
Id string `json:"id"`
Type string `json:"type"`
Attributes struct {
Ttml string `json:"ttml"`
PlayParams struct {
Id string `json:"id"`
Kind string `json:"kind"`
CatalogId string `json:"catalogId"`
DisplayType int `json:"displayType"`
} `json:"playParams"`
} `json:"attributes"`
} `json:"data"`
}