package main

import (
“bufio”
“bytes”
“encoding/binary”
“encoding/json”
“gopkg.in/yaml.v2”
“errors”
“fmt”
“io”
“io/ioutil”
“math”
“net”
“net/http”
“net/url”
“os”
“os/exec”
“path/filepath”
“regexp”
“sort”
“strconv”
“strings”
“time”

“github.com/abema/go-mp4”
“github.com/beevik/etree”
“github.com/grafov/m3u8”
)

const (
defaultId = “0”
prefetchKey = “skd://itunes.apple.com/P000000000/s1/e1”
)

var (
forbiddenNames = regexp.MustCompile([/\\&lt;&gt;:"|?*])
)
type Config struct {
MediaUserToken string yaml:"media-user-token"
SaveLrcFile bool yaml:"save-lrc-file"
EmbedLrc bool yaml:"embed-lrc"
EmbedCover bool yaml:"embed-cover"
CoverSize string yaml:"cover-size"
CoverFormat string yaml:"cover-format"
AlacSaveFolder string yaml:"alac-save-folder"
AtmosSaveFolder string yaml:"atmos-save-folder"
Check string yaml:"check"
}

var config Config
var txtpath string
var oktrackNum int = 0
var trackTotalnum int = 0

type SampleInfo struct {
data []byte
duration uint32
descIndex uint32
}

type SongInfo struct {
r io.ReadSeeker
alacParam *Alac
samples []SampleInfo
}
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 (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 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
}

err = addMeta(mp4.BoxType{‘\251’, ‘a’, ‘l’, ‘b’}, meta.Data[0].Attributes.Name)
if err != nil {
return err
}

err = addMeta(mp4.BoxType{‘s’, ‘o’, ‘a’, ‘l’}, meta.Data[0].Attributes.Name)
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 = 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
}

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’}, album.Attributes.ArtistName)
if err != nil {
return err
}

err = addMeta(mp4.BoxType{‘s’, ‘o’, ‘a’, ‘a’}, album.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
}

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].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)
binary.BigEndian.PutUint32(trkn, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.TrackNumber))
binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal))
err = addMeta(mp4.BoxType{‘t’, ‘r’, ‘k’, ‘n’}, trkn)
if err != nil {
return err
}
if meta.Data[0].Relationships.Tracks.Data[trackTotal - 1].Attributes.DiscNumber > 1{
disk := make([]byte, 8)
binary.BigEndian.PutUint32(disk, uint32(meta.Data[0].Relationships.Tracks.Data[index].Attributes.DiscNumber))
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”, “127.0.0.1:10020”)
if err != nil {
return err
}
defer conn.Close()
var decrypted []byte
var lastIndex uint32 = math.MaxUint8

fmt.Println(“Decrypt start.”)
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…)
}
, _ = conn.Write([]byte{0, 0, 0, 0, 0})

fmt.Println(“Decrypt finished.”)

create, err := os.Create(filename)
if err != nil {
return err
}
defer create.Close()

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 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”)
query.Set(“fields[albums:albums]”, “artistName,artwork,name,releaseDate,url”)
query.Set(“fields[record-labels]”, “name”)
// 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/lyrics”, storefront, songId), 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, url string) error {
covPath := filepath.Join(sanAlbumFolder, “cover.” + config.CoverFormat)
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)
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 rip(albumId string, token string, storefront string, userToken string) error {
meta, err := getMeta(albumId, token, storefront)
if err != nil {
fmt.Println(“Failed to get album metadata.\n”)
return err
}
singerFoldername := fmt.Sprintf(“%s”, meta.Data[0].Attributes.ArtistName)
if strings.HasSuffix(singerFoldername, “.”) {
singerFoldername = strings.ReplaceAll(singerFoldername, “.”, “”)
}
singerFoldername = strings.TrimSpace(singerFoldername)
singerFolder := filepath.Join(config.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "“))
albumFolder := fmt.Sprintf(”%s", meta.Data[0].Attributes.Name)
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(singerFoldername)
fmt.Println(albumFolder)
err = writeCover(sanAlbumFolder, meta.Data[0].Attributes.Artwork.URL)
if err != nil {
fmt.Println(“Failed to write cover.”)
}
trackTotal := len(meta.Data[0].Relationships.Tracks.Data)
for trackNum, track := range meta.Data[0].Relationships.Tracks.Data {
trackNum++
trackTotalnum += 1
fmt.Printf(“Track %d of %d:\n”, trackNum, trackTotal)
manifest, err := getInfoFromAdam(track.ID, token, storefront)
if err != nil {
fmt.Println(“Failed to get manifest.\n”, err)
continue
}
if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == “” {
fmt.Println(“Unavailable in ALAC.”)
continue
}
filename := fmt.Sprintf(”%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "“))
lrcFilename := fmt.Sprintf(”%02d. %s.lrc", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "“))
trackPath := filepath.Join(sanAlbumFolder, filename)
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.”)
oktrackNum += 1
continue
}
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”, track.ID)
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”{
manifest.Attributes.ExtendedAssetUrls.EnhancedHls=string(Checkbody)
fmt.Println(“Found m3u8 from API”)
}
}
}
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, track.ID) {
parts := strings.SplitN(line, “,”, 2)
if len(parts) == 2 {
manifest.Attributes.ExtendedAssetUrls.EnhancedHls=parts[1]
fmt.Println(“Found m3u8 from txt”)
}
}
}
if err := scanner.Err(); err != nil {
fmt.Println(err)
}
}
trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls)
if err != nil {
fmt.Println(“Failed to extract info from manifest.\n”, err)
continue
}
info, err := extractSong(trackUrl)
if err != nil {
fmt.Println(“Failed to extract track.”, err)
continue
}
samplesOk := true
for samplesOk {
for , i := range info.samples {
if int(i.descIndex) >= len(keys) {
fmt.Println(“Decryption size mismatch.”)
samplesOk = false
}
}
break
}
if !samplesOk {
continue
}
err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal)
if err != nil {
fmt.Println(“Failed to decrypt track.\n”, err)
continue
}
tags := []string{
fmt.Sprintf(“lyrics=%s”, lrc),
}
if config.EmbedCover {
tags = append(tags, fmt.Sprintf(“cover=%s/cover.%s”, sanAlbumFolder, config.CoverFormat))
}
tagsString := strings.Join(tags, “:”)
cmd := exec.Command(“MP4Box”, “-itags”, tagsString, trackPath)
if err := cmd.Run(); err != nil {
fmt.Printf(“Embed failed: %v\n”, err)
continue
}
oktrackNum += 1
}
return err
}

func main() {
err := loadConfig()
if err != nil {
fmt.Printf(“load config failed: %v”, err)
return
}
token, err := getToken()
if err != nil {
fmt.Println(“Failed to get token.”)
return
}
albumTotal := len(os.Args[1:])
for albumNum, url := range os.Args[1:] {
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(“======= Completed %d/%d ###### %d errors!! =======\n”, oktrackNum, trackTotalnum, trackTotalnum-oktrackNum)
}

func conventTTMLToLRC(ttml string) (string, error) {
parsedTTML := etree.NewDocument()
err := parsedTTML.ReadFromString(ttml)
if err != nil {
return “”, err
}
var lrcLines []string
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)
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 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 variant.Codecs == “alac” {
split := strings.Split(variant.Audio, “-”)
length := len(split)
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 alac 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 strings.HasSuffix(match[1], “c23”) || strings.HasSuffix(match[1], “c6”) {
keys = append(keys, match[1])
}
}
return streamUrl.String(), keys, 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)
}
rawSong, err := ioutil.ReadAll(track.Body)
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
}

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),
}

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"
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"
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"
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"
} 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"
} 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"
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"
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 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"
}

this is executed by go run main.go https://music.apple.com/us/album/happiness-begins/1461478261

modify this script to save that above link in apple_music_link.txt by creating that text document and save that link