diff --git a/.gitignore b/.gitignore index cd35375..3b2c8cf 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ !go.sum !main.go !README.md +!utils/ diff --git a/README.md b/README.md index 0424557..916aaf4 100644 --- a/README.md +++ b/README.md @@ -7,14 +7,14 @@ 3. 运行结束后显示总体完成情况 4. 自动内嵌封面和LRC歌词(需要media-user-token,获取方式看最后的说明) 5. 自动构建 可以到 [Actions](https://github.com/zhaarey/apple-music-alac-atmos-downloader/actions) 页面下载最新自动构建版本 可以直接`main.exe url` -6. main 支持check 可以填入文本地址 或API数据库. +6. 支持逐词与未同步歌词 7. 新增get-m3u8-from-device 改为true 且设置端口`adb forward tcp:20020 tcp:20020`即从模拟器获取m3u8 8. 文件夹和文件支持模板 9. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` 自动选择歌手的所有专辑 10. 新增[wrapper](https://github.com/zhaarey/wrapper/releases)模式 目前只能linux运行,解密速度超快,基本秒解 11. `limit-max`支持限制长度 默认200 -12. 支持逐词与未同步歌词 -13. 现已支持arm64解密 +12. 现已支持arm64解密 +13. 下载解密部分更换为Sendy McSenderson的代码,实现边下载边解密 ### Special thanks to `chocomint` for creating `agent-arm64.js` diff --git a/config.yaml b/config.yaml index ea0fc37..e03815f 100644 --- a/config.yaml +++ b/config.yaml @@ -13,8 +13,7 @@ cover-size: 5000x5000 cover-format: jpg #jpg png or original alac-save-folder: AM-DL downloads atmos-save-folder: AM-DL-Atmos downloads -check: "" # API or .txt -force-api: false +max-memory-limit: 256 # MB decrypt-m3u8-port: "127.0.0.1:10020" get-m3u8-port: "127.0.0.1:20020" get-m3u8-from-device: true diff --git a/go.mod b/go.mod index 3700ae2..0ebe02a 100644 --- a/go.mod +++ b/go.mod @@ -3,15 +3,16 @@ module main go 1.17 require ( - github.com/abema/go-mp4 v0.7.2 + github.com/Eyevinn/mp4ff v0.46.0 + github.com/abema/go-mp4 v1.3.0 github.com/grafov/m3u8 v0.11.1 + github.com/schollz/progressbar/v3 v3.14.6 github.com/spf13/pflag v1.0.5 ) require ( github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect github.com/rivo/uniseg v0.4.7 // indirect - github.com/schollz/progressbar/v3 v3.14.6 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect ) diff --git a/go.sum b/go.sum index 838c672..9695a86 100644 --- a/go.sum +++ b/go.sum @@ -1,11 +1,15 @@ -github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg= -github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= +github.com/Eyevinn/mp4ff v0.46.0 h1:A8oJA4A3C9fDbX38jEw/26utjNdvmRmrO37tVI5pDk0= +github.com/Eyevinn/mp4ff v0.46.0/go.mod h1:hJNUUqOBryLAzUW9wpCJyw2HaI+TCd2rUPhafoS5lgg= +github.com/abema/go-mp4 v1.3.0 h1:vr0PX0jk3E4GO1c28fNRsyZdkLwz38R+XRVncIH1XDk= +github.com/abema/go-mp4 v1.3.0/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= github.com/beevik/etree v1.3.0 h1:hQTc+pylzIKDb23yYprodCWWTt+ojFfUZyzU09a/hmU= github.com/beevik/etree v1.3.0/go.mod h1:aiPf89g/1k3AShMVAzriilpcE4R/Vuor90y83zVZWFc= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-test/deep v1.1.0 h1:WOcxcdHcvdgThNXjw0t76K42FXTU7HpNQWHpA2HHNlg= +github.com/go-test/deep v1.1.0/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= diff --git a/main.go b/main.go index 54e4aa6..c40c27d 100644 --- a/main.go +++ b/main.go @@ -1,3129 +1,1368 @@ -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"` - Language string `yaml:"language"` - SaveLrcFile bool `yaml:"save-lrc-file"` - LrcType string `yaml:"lrc-type"` - 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") - query := url.Values{} - query.Set("l", config.Language) - 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&l=%s", storefront, artistId, Num, config.Language), 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", config.Language) - 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&l=%s", storefront, mtype, albumId, pageStr, config.Language), 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.LrcType), 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.%s", forbiddenNames.ReplaceAllString(songName, "_"), config.LrcFormat) - 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 if config.LrcFormat == "ttml"{ - if config.SaveLrcFile { - lrc = ttml - err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) - if err != nil { - fmt.Printf("Failed to write lyrics") - } - lrc = "" - } - } 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)) - trackPath = m4atrackPath - } - if !(checkSongIntegrity(trackPath, manifest.Attributes.DurationInMillis)) { - fmt.Println("Audio Integrity : Bad") - if err := os.Remove(trackPath); err != nil { - fmt.Printf("Error deleting file: %v\n", err) - counter.Error++ - continue - } - counter.Error++ - continue - } - fmt.Println("Audio Integrity : OK") - 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 checkSongIntegrity(songPath string, duration int) bool { - duration = duration / 1000 - starttime := strconv.Itoa(duration - 2) - command := "ffmpeg" - args := []string{"-y", "-v", "error", "-ss", starttime, "-i", songPath, "-map", "0:a", "-c:a", "pcm_s16le", "-f", "null", "NUL"} - cmd := exec.Command(command, args...) - var stderr bytes.Buffer - cmd.Stderr = &stderr - err := cmd.Run() - return err == nil && stderr.Len() == 0 -} - -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") - query.Set("l", config.Language) - 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"` - DurationInMillis int `json:"durationInMillis"` -} - -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"` -} +package main + +import ( + "bufio" + "bytes" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net" + "net/http" + "net/url" + "os" + "os/exec" + "path/filepath" + "regexp" + "sort" + "strconv" + "strings" + + "github.com/spf13/pflag" + + "gopkg.in/yaml.v2" + + "github.com/beevik/etree" + "github.com/grafov/m3u8" + + "main/utils/runv2" + "main/utils/structs" + +) + +var ( + forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) + dl_atmos bool + dl_select bool + artist_select bool + alac_max *int + atmos_max *int + Config structs.ConfigSet + counter structs.Counter + okDict = make(map[string][]int) +) + + + +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 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 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") + query := url.Values{} + query.Set("l", Config.Language) + 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(structs.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&l=%s", storefront, artistId, Num, Config.Language), 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(structs.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) (*structs.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", Config.Language) + 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(structs.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&l=%s", storefront, mtype, albumId, pageStr, Config.Language), 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(structs.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.LrcType), 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(structs.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, "_")) + lrcFilename := fmt.Sprintf("%s.%s", forbiddenNames.ReplaceAllString(songName, "_"), Config.LrcFormat) + 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 if Config.LrcFormat == "ttml"{ + if Config.SaveLrcFile { + lrc = ttml + err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) + if err != nil { + fmt.Printf("Failed to write lyrics") + } + lrc = "" + } + } 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 + } + + trackM3u8Url, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) + if err != nil { + fmt.Println("\u26A0 Failed to extract info from manifest:", err) + counter.Unavailable++ + continue + } + //边下载边解密 + err = runv2.Run(track.ID, trackM3u8Url, trackPath, Config) + if err != nil { + fmt.Println("Failed to run v2:", err) + counter.Error++ + continue + } + + + //add tags + index := trackNum - 1 + 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 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 + } + } + 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, "/playlist/") { + storefront, albumId = checkUrlPlaylist(url) + } else { + storefront, albumId = checkUrl(url) + } + 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 = structs.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.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") + } + } + 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, error) { + masterUrl, err := url.Parse(b) + if err != nil { + return "", err + } + 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) + 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 "", 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 "", 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 "", errors.New("no codec found") + } + return streamUrl.String(), 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 getInfoFromAdam(adamId string, token string, storefront string) (*structs.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") + query.Set("l", Config.Language) + 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(structs.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 +}