diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index e85332a..15879c2 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -19,8 +19,6 @@ jobs: - name: Build run: | go build -o main.exe -v ./main.go - go build -o main_atmos.exe -v ./main_atmos.go - go build -o main_select.exe -v ./main_select.go - name: Create a new directory and copy files run: | mkdir -p alac @@ -28,8 +26,6 @@ jobs: cp config.yaml alac/ cp README.md alac/ cp main.exe alac/ - cp main_atmos.exe alac/ - cp main_select.exe alac/ - name: Upload apple-music-alac-atmos-downloader uses: actions/upload-artifact@v2 with: @@ -47,8 +43,6 @@ jobs: - name: Build run: | go build -o main -v ./main.go - go build -o main_atmos -v ./main_atmos.go - go build -o main_select -v ./main_select.go - name: Create a new directory and copy files run: | mkdir -p alac @@ -56,8 +50,6 @@ jobs: cp config.yaml alac/ cp README.md alac/ cp main alac/ - cp main_atmos alac/ - cp main_select alac/ - name: Upload apple-music-alac-atmos-downloader uses: actions/upload-artifact@v2 with: diff --git a/.gitignore b/.gitignore index 8f8dda6..9de1fcb 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,4 @@ !go.mod !go.sum !main.go -!main_atmos.go -!main_select.go !README.md diff --git a/main.go b/main.go index 2d24a02..09e6702 100644 --- a/main.go +++ b/main.go @@ -1376,7 +1376,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro fmt.Println("Unavailable.\n") } else { EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album") - if strings.HasPrefix(EnhancedHls_m3u8, "http") { + if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") { manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 } Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) @@ -1538,7 +1538,7 @@ func rip(albumId string, token string, storefront string, userToken string) erro continue } EnhancedHls_m3u8, err := checkM3u8(track.ID, "song") - if strings.HasPrefix(EnhancedHls_m3u8, "http") { + if strings.HasSuffix(EnhancedHls_m3u8, ".m3u8") { manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 } var Quality string @@ -1819,6 +1819,9 @@ func conventTTMLToLRC(ttml string) (string, error) { _, 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 { diff --git a/main_atmos.go b/main_atmos.go deleted file mode 100644 index fff86ed..0000000 --- a/main_atmos.go +++ /dev/null @@ -1,2399 +0,0 @@ -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/beevik/etree" - "gopkg.in/yaml.v2" - - "github.com/abema/go-mp4" - "github.com/grafov/m3u8" -) - -const ( - defaultId = "0" - prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" -) - -var ( - forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) -) - -type Config struct { - MediaUserToken string `yaml:"media-user-token"` - SaveLrcFile bool `yaml:"save-lrc-file"` - 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"` - DecryptM3u8Port string `yaml:"decrypt-m3u8-port"` - GetM3u8Port string `yaml:"get-m3u8-port"` - AtmosMax int `yaml:"atmos-max"` - UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` - DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` -} - -var config Config -var oktrackNum int = 0 -var trackTotalnum int = 0 - -type SampleInfo struct { - data []byte - duration uint32 - descIndex uint32 -} - -type SongInfo struct { - r io.ReadSeeker - alacParam *Alac - samples []SampleInfo -} - -func loadConfig() error { - // 读取config.yaml文件内容 - data, err := ioutil.ReadFile("config.yaml") - if err != nil { - return err - } - // 将yaml解析到config变量中 - err = yaml.Unmarshal(data, &config) - if err != nil { - return err - } - return nil -} - -func (s *SongInfo) Duration() (ret uint64) { - for i := range s.samples { - ret += uint64(s.samples[i].duration) - } - return -} - -func (*Alac) GetType() mp4.BoxType { - return BoxTypeAlac() -} - -func fileExists(path string) (bool, error) { - f, err := os.Stat(path) - if err == nil { - return !f.IsDir(), nil - } else if os.IsNotExist(err) { - return false, nil - } - return false, err -} - -func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error { - index := trackNum - 1 - { // ftyp - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()}) - if err != nil { - return err - } - _, err = mp4.Marshal(w, &mp4.Ftyp{ - MajorBrand: [4]byte{'M', '4', 'A', ' '}, - MinorVersion: 0, - CompatibleBrands: []mp4.CompatibleBrandElem{ - {CompatibleBrand: [4]byte{'M', '4', 'A', ' '}}, - {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, - {CompatibleBrand: mp4.BrandISOM()}, - {CompatibleBrand: [4]byte{0, 0, 0, 0}}, - }, - }, box.Context) - if err != nil { - return err - } - _, err = w.EndBox() - if err != nil { - return err - } - } - - const chunkSize uint32 = 5 - duration := info.Duration() - numSamples := uint32(len(info.samples)) - var stco *mp4.BoxInfo - - { // moov - _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()}) - if err != nil { - return err - } - box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()}) - if err != nil { - return err - } - moovOri := box[0] - - { // mvhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()}) - if err != nil { - return err - } - mvhd := oriBox[0].Payload.(*mp4.Mvhd) - if mvhd.Version == 0 { - mvhd.DurationV0 = uint32(duration) - } else { - mvhd.DurationV1 = duration - } - - _, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // trak - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()}) - if err != nil { - return err - } - trakOri := box[0] - - { // tkhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()}) - if err != nil { - return err - } - tkhd := oriBox[0].Payload.(*mp4.Tkhd) - if tkhd.Version == 0 { - tkhd.DurationV0 = uint32(duration) - } else { - tkhd.DurationV1 = duration - } - tkhd.SetFlags(0x7) - - _, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // mdia - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()}) - if err != nil { - return err - } - mdiaOri := box[0] - - { // mdhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()}) - if err != nil { - return err - } - mdhd := oriBox[0].Payload.(*mp4.Mdhd) - if mdhd.Version == 0 { - mdhd.DurationV0 = uint32(duration) - } else { - mdhd.DurationV1 = duration - } - - _, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // hdlr - oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()}) - if err != nil { - return err - } - - err = w.CopyBox(info.r, oriBox[0]) - if err != nil { - return err - } - } - - { // minf - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()}) - if err != nil { - return err - } - minfOri := box[0] - - { // smhd, dinf - boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{ - {mp4.BoxTypeSmhd()}, - {mp4.BoxTypeDinf()}, - }) - if err != nil { - return err - } - - for _, b := range boxes { - err = w.CopyBox(info.r, b) - if err != nil { - return err - } - } - } - - { // stbl - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()}) - if err != nil { - return err - } - - { // stsd - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()}) - if err != nil { - return err - } - _, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context) - if err != nil { - return err - } - - { // alac - _, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) - if err != nil { - return err - } - - _, err = w.Write([]byte{ - 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 0}) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels)) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth)) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0}) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0}) - if err != nil { - return err - } - - box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, info.alacParam, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stts - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()}) - if err != nil { - return err - } - - var stts mp4.Stts - for _, sample := range info.samples { - if len(stts.Entries) != 0 { - last := &stts.Entries[len(stts.Entries)-1] - if last.SampleDelta == sample.duration { - last.SampleCount++ - continue - } - } - stts.Entries = append(stts.Entries, mp4.SttsEntry{ - SampleCount: 1, - SampleDelta: sample.duration, - }) - } - stts.EntryCount = uint32(len(stts.Entries)) - - _, err = mp4.Marshal(w, &stts, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stsc - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()}) - if err != nil { - return err - } - - if numSamples%chunkSize == 0 { - _, err = mp4.Marshal(w, &mp4.Stsc{ - EntryCount: 1, - Entries: []mp4.StscEntry{ - { - FirstChunk: 1, - SamplesPerChunk: chunkSize, - SampleDescriptionIndex: 1, - }, - }, - }, box.Context) - } else { - _, err = mp4.Marshal(w, &mp4.Stsc{ - EntryCount: 2, - Entries: []mp4.StscEntry{ - { - FirstChunk: 1, - SamplesPerChunk: chunkSize, - SampleDescriptionIndex: 1, - }, { - FirstChunk: numSamples/chunkSize + 1, - SamplesPerChunk: numSamples % chunkSize, - SampleDescriptionIndex: 1, - }, - }, - }, box.Context) - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stsz - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()}) - if err != nil { - return err - } - - stsz := mp4.Stsz{SampleCount: numSamples} - for _, sample := range info.samples { - stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data))) - } - - _, err = mp4.Marshal(w, &stsz, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stco - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()}) - if err != nil { - return err - } - - l := (numSamples + chunkSize - 1) / chunkSize - _, err = mp4.Marshal(w, &mp4.Stco{ - EntryCount: l, - ChunkOffset: make([]uint32, l), - }, box.Context) - - stco, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // udta - ctx := mp4.Context{UnderUdta: true} - _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx}) - if err != nil { - return err - } - - { // meta - ctx.UnderIlstMeta = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, &mp4.Meta{}, ctx) - if err != nil { - return err - } - - { // hdlr - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, &mp4.Hdlr{ - HandlerType: [4]byte{'m', 'd', 'i', 'r'}, - Reserved: [3]uint32{0x6170706c, 0, 0}, - }, ctx) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // ilst - ctx.UnderIlst = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx}) - if err != nil { - return err - } - - marshalData := func(val interface{}) error { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) - if err != nil { - return err - } - - var boxData mp4.Data - switch v := val.(type) { - case string: - boxData.DataType = mp4.DataTypeStringUTF8 - boxData.Data = []byte(v) - case uint8: - boxData.DataType = mp4.DataTypeSignedIntBigEndian - boxData.Data = []byte{v} - case uint32: - boxData.DataType = mp4.DataTypeSignedIntBigEndian - boxData.Data = make([]byte, 4) - binary.BigEndian.PutUint32(boxData.Data, v) - case []byte: - boxData.DataType = mp4.DataTypeBinary - boxData.Data = v - default: - panic("unsupported value") - } - - _, err = mp4.Marshal(w, &boxData, ctx) - if err != nil { - return err - } - - _, err = w.EndBox() - return err - } - - addMeta := func(tag mp4.BoxType, val interface{}) error { - _, err = w.StartBox(&mp4.BoxInfo{Type: tag}) - if err != nil { - return err - } - - err = marshalData(val) - if err != nil { - return err - } - - _, err = w.EndBox() - return err - } - - addExtendedMeta := func(name string, val interface{}) error { - ctx.UnderIlstFreeMeta = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx}) - if err != nil { - return err - } - - { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx}) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - - _, err = io.WriteString(w, "com.apple.iTunes") - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx}) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - - _, err = io.WriteString(w, name) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - err = marshalData(val) - if err != nil { - return err - } - - ctx.UnderIlstFreeMeta = false - - _, err = w.EndBox() - return err - } - - err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, meta.Data[0].Attributes.Name) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'\251', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, strings.Split(meta.Data[0].Attributes.ReleaseDate, "-")[0]) - if err != nil { - return err - } - - // cnID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].ID, 10, 32) - // if err != nil { - // return err - // } - - // err = addMeta(mp4.BoxType{'c', 'n', 'I', 'D'}, uint32(cnID)) - // if err != nil { - // return err - // } - - err = addExtendedMeta("ISRC", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc) - if err != nil { - return err - } - - if len(meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames) > 0 { - err = addMeta(mp4.BoxType{'\251', 'g', 'e', 'n'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]) - if err != nil { - return err - } - } - - if len(meta.Data) > 0 { - album := meta.Data[0] - - err = addMeta(mp4.BoxType{'a', 'A', 'R', 'T'}, album.Attributes.ArtistName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'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 = addExtendedMeta("LABEL", album.Attributes.RecordLabel) - if err != nil { - return err - } - - err = addExtendedMeta("UPC", album.Attributes.Upc) - if err != nil { - return err - } - - // plID, err := strconv.ParseUint(album.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.Artists.Data) > 0 { - // atID, err := strconv.ParseUint(meta.Data[0].Relationships.Artists.Data[index].ID, 10, 32) - // if err != nil { - // return err - // } - - // err = addMeta(mp4.BoxType{'a', 't', 'I', 'D'}, uint32(atID)) - // if err != nil { - // return err - // } - // } - - trkn := make([]byte, 8) - binary.BigEndian.PutUint32(trkn, uint32(trackNum)) - binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) - err = addMeta(mp4.BoxType{'t', 'r', 'k', 'n'}, trkn) - if err != nil { - return err - } - - // disk := make([]byte, 8) - // binary.BigEndian.PutUint32(disk, uint32(meta.Attributes.DiscNumber)) - // err = addMeta(mp4.BoxType{'d', 'i', 's', 'k'}, disk) - // if err != nil { - // return err - // } - - ctx.UnderIlst = false - - _, err = w.EndBox() - if err != nil { - return err - } - } - - ctx.UnderIlstMeta = false - _, err = w.EndBox() - if err != nil { - return err - } - } - - ctx.UnderUdta = false - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdat()}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, &mp4.Mdat{Data: data}, box.Context) - if err != nil { - return err - } - - mdat, err := w.EndBox() - - var realStco mp4.Stco - - offset := mdat.Offset + mdat.HeaderSize - for i := uint32(0); i < numSamples; i++ { - if i%chunkSize == 0 { - realStco.EntryCount++ - realStco.ChunkOffset = append(realStco.ChunkOffset, uint32(offset)) - } - offset += uint64(len(info.samples[i].data)) - } - - _, err = stco.SeekToPayload(w) - if err != nil { - return err - } - _, err = mp4.Marshal(w, &realStco, box.Context) - if err != nil { - return err - } - } - - return nil -} - -func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filename string, trackNum, trackTotal int) error { - //fmt.Printf("%d-bit / %d Hz\n", info.bitDepth, info.bitRate) - conn, err := net.Dial("tcp", config.DecryptM3u8Port) - if err != nil { - return err - } - defer conn.Close() - var decrypted []byte - var lastIndex uint32 = math.MaxUint8 - - fmt.Println("Decrypt start.") - for _, sp := range info.samples { - if lastIndex != sp.descIndex { - if len(decrypted) != 0 { - _, err := conn.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - } - keyUri := keys[sp.descIndex] - id := manifest.Data[0].Relationships.Tracks.Data[trackNum-1].ID - if keyUri == prefetchKey { - id = defaultId - } - - _, err := conn.Write([]byte{byte(len(id))}) - if err != nil { - return err - } - _, err = io.WriteString(conn, id) - if err != nil { - return err - } - - _, err = conn.Write([]byte{byte(len(keyUri))}) - if err != nil { - return err - } - _, err = io.WriteString(conn, keyUri) - if err != nil { - return err - } - } - lastIndex = sp.descIndex - - err := binary.Write(conn, binary.LittleEndian, uint32(len(sp.data))) - if err != nil { - return err - } - - _, err = conn.Write(sp.data) - if err != nil { - return err - } - - de := make([]byte, len(sp.data)) - _, err = io.ReadFull(conn, de) - if err != nil { - return err - } - - decrypted = append(decrypted, de...) - } - _, _ = conn.Write([]byte{0, 0, 0, 0, 0}) - - fmt.Println("Decrypt finished.") - - file, err := os.Create(filename) - if err != nil { - panic(err) - } - defer file.Close() - - _, err = file.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 checkArtist(artistUrl string, token string) ([]string, error) { - storefront, artistId := checkUrlArtist(artistUrl) - Num := 0 - - var args []string - var urls []string - var options []string - for { - req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/artists/%s/albums?limit=100&offset=%d", storefront, artistId, Num), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Origin", "https://music.apple.com") - do, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - obj := new(AutoGeneratedArtist) - err = json.NewDecoder(do.Body).Decode(&obj) - if err != nil { - return nil, err - } - for _, album := range obj.Data { - urls = append(urls, album.Attributes.URL) - options = append(options, fmt.Sprintf("%s(%s)", album.Attributes.Name, album.ID)) - } - Num = Num + 100 - if len(obj.Next) == 0 { - break - } - } - for i, option := range options { - fmt.Printf("%02d: %s\n", i+1, option) - } - reader := bufio.NewReader(os.Stdin) - fmt.Println("Please select from the following options (multiple options separated by commas, ranges supported, or type 'all' to select all)") - fmt.Print("Enter your choice: ") - input, _ := reader.ReadString('\n') - - // Remove newline and whitespace - input = strings.TrimSpace(input) - if input == "all" { - fmt.Println("You have selected all options:") - return urls, nil - } - - // Split input into string slices - selectedOptions := [][]string{} - parts := strings.Split(input, ",") - for _, part := range parts { - if strings.Contains(part, "-") { // Range setting - rangeParts := strings.Split(part, "-") - selectedOptions = append(selectedOptions, rangeParts) - } else { // Single option - selectedOptions = append(selectedOptions, []string{part}) - } - } - - // Print selected options - fmt.Println("You have selected the following options:") - for _, opt := range selectedOptions { - if len(opt) == 1 { // Single option - num, err := strconv.Atoi(opt[0]) - if err != nil { - fmt.Println("Invalid option:", opt[0]) - continue - } - if num > 0 && num <= len(options) { - fmt.Println(options[num-1]) - args = append(args, urls[num-1]) - } else { - fmt.Println("Option out of range:", opt[0]) - } - } else if len(opt) == 2 { // Range - start, err1 := strconv.Atoi(opt[0]) - end, err2 := strconv.Atoi(opt[1]) - if err1 != nil || err2 != nil { - fmt.Println("Invalid range:", opt) - continue - } - if start < 1 || end > len(options) || start > end { - fmt.Println("Range out of range:", opt) - continue - } - for i := start; i <= end; i++ { - fmt.Println(options[i-1]) - args = append(args, urls[i-1]) - } - } else { - fmt.Println("Invalid option:", opt) - } - } - return args, nil -} - -func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { - var mtype string - var page int - if strings.Contains(albumId, "pl.") { - mtype = "playlists" - } else { - mtype = "albums" - } - req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Origin", "https://music.apple.com") - query := url.Values{} - query.Set("omit[resource]", "autos") - query.Set("include", "tracks,artists,record-labels") - query.Set("include[songs]", "artists") - query.Set("fields[artists]", "name,artwork") - query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url") - query.Set("fields[record-labels]", "name") - query.Set("extend", "editorialVideo") - // query.Set("l", "en-gb") - req.URL.RawQuery = query.Encode() - do, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - obj := new(AutoGenerated) - err = json.NewDecoder(do.Body).Decode(&obj) - if err != nil { - return nil, err - } - if strings.Contains(albumId, "pl.") { - obj.Data[0].Attributes.ArtistName = "Apple Music" - if len(obj.Data[0].Relationships.Tracks.Next) > 0 { - page = 0 - for { - page = page + 100 - pageStr := strconv.Itoa(page) - req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Origin", "https://music.apple.com") - do, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - obj2 := new(AutoGeneratedTrack) - err = json.NewDecoder(do.Body).Decode(&obj2) - if err != nil { - return nil, err - } - for _, value := range obj2.Data { - obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) - } - if len(obj2.Next) == 0 { - break - } - } - } - } - return obj, nil -} - -func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) { - req, err := http.NewRequest("GET", - fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/lyrics", storefront, songId), nil) - if err != nil { - return "", err - } - req.Header.Set("Origin", "https://music.apple.com") - req.Header.Set("Referer", "https://music.apple.com/") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - cookie := http.Cookie{Name: "media-user-token", Value: userToken} - req.AddCookie(&cookie) - do, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer do.Body.Close() - obj := new(SongLyrics) - err = json.NewDecoder(do.Body).Decode(&obj) - if obj.Data != nil { - return obj.Data[0].Attributes.Ttml, nil - } else { - return "", errors.New("failed to get lyrics") - } -} - -func writeCover(sanAlbumFolder, name string, url string) error { - covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat) - exists, err := fileExists(covPath) - if err != nil { - fmt.Println("Failed to check if cover exists.") - return err - } - if exists { - return nil - } - if config.CoverFormat == "png" { - re := regexp.MustCompile(`\{w\}x\{h\}`) - parts := re.Split(url, 2) - url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1) - } - url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - do, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - errors.New(do.Status) - } - f, err := os.Create(covPath) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, do.Body) - if err != nil { - return err - } - return nil -} - -func writeLyrics(sanAlbumFolder, filename string, lrc string) error { - lyricspath := filepath.Join(sanAlbumFolder, filename) - f, err := os.Create(lyricspath) - if err != nil { - return err - } - defer f.Close() - _, err = f.WriteString(lrc) - if err != nil { - return err - } - return nil -} - -func rip(albumId string, token string, storefront string, userToken string) error { - meta, err := getMeta(albumId, token, storefront) - if err != nil { - fmt.Println("Failed to get album metadata.\n") - return err - } - var singerFoldername string - if config.ArtistFolderFormat != "" { - if strings.Contains(albumId, "pl.") { - singerFoldername = strings.NewReplacer( - "{ArtistName}", "Apple Music", - "{ArtistId}", "", - ).Replace(config.ArtistFolderFormat) - } else if len(meta.Data[0].Relationships.Artists.Data) > 0 { - singerFoldername = strings.NewReplacer( - "{ArtistName}", meta.Data[0].Attributes.ArtistName, - "{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID, - ).Replace(config.ArtistFolderFormat) - } else { - singerFoldername = strings.NewReplacer( - "{ArtistName}", 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.AtmosSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) - 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 - Quality := fmt.Sprintf("%dkbps", config.AtmosMax-2000) - if strings.Contains(albumId, "pl.") { - albumFolder = strings.NewReplacer( - "{ArtistName}", "Apple Music", - "{PlaylistName}", meta.Data[0].Attributes.Name, - "{PlaylistId}", albumId, - "{Quality}", Quality, - "{Codec}", "Atmos", - "{Tag}", Tag_string, - ).Replace(config.PlaylistFolderFormat) - } else { - albumFolder = strings.NewReplacer( - "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, - "{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4], - "{ArtistName}", meta.Data[0].Attributes.ArtistName, - "{AlbumName}", meta.Data[0].Attributes.Name, - "{UPC}", meta.Data[0].Attributes.Upc, - "{Copyright}", meta.Data[0].Attributes.Copyright, - "{AlbumId}", albumId, - "{Quality}", Quality, - "{Codec}", "Atmos", - "{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) - for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { - trackNum++ - trackTotalnum += 1 - fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) - manifest, err := getInfoFromAdam(track.ID, token, storefront) - if err != nil { - fmt.Println("Failed to get manifest.\n", err) - continue - } - if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { - fmt.Println("Unavailable in ALAC.") - continue - } - - 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}", track.Attributes.Name, - "{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber), - "{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber), - "{Quality}", Quality, - "{Codec}", "Atmos", - "{Tag}", Tag_string, - ).Replace(config.SongFileFormat) - fmt.Println(songName) - filename := fmt.Sprintf("%s.ec3", forbiddenNames.ReplaceAllString(songName, "_")) - m4afilename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) - lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_")) - trackPath := filepath.Join(sanAlbumFolder, filename) - m4atrackPath := filepath.Join(sanAlbumFolder, m4afilename) - exists, err := fileExists(trackPath) - m4aexists, errs := fileExists(m4atrackPath) - if err != nil { - fmt.Println("Failed to check if track exists.") - } - if errs != nil { - fmt.Println("Failed to check if m4atrack exists.") - } - if exists || m4aexists { - fmt.Println("Track or M4atrack already exists locally.") - oktrackNum += 1 - continue - } - var lrc string = "" - if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) { - ttml, err := getSongLyrics(track.ID, storefront, token, userToken) - if err != nil { - fmt.Println("Failed to get lyrics") - } else { - lrc, err = conventTTMLToLRC(ttml) - if err != nil { - fmt.Printf("Failed to parse lyrics: %s \n", err) - } else { - if config.SaveLrcFile { - err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) - if err != nil { - fmt.Printf("Failed to write lyrics") - } - if !config.EmbedLrc { - lrc = "" - } - } - } - } - } - trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) - if err != nil { - fmt.Println("Failed to extract info from manifest.\n", err) - continue - } - info, err := extractSong(trackUrl) - if err != nil { - fmt.Println("Failed to extract track.", err) - continue - } - samplesOk := true - for samplesOk { - for _, i := range info.samples { - if int(i.descIndex) >= len(keys) { - fmt.Println("Decryption size mismatch.") - samplesOk = false - } - } - break - } - if !samplesOk { - continue - } - err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) - if err != nil { - fmt.Println("Failed to decrypt track.\n", err) - continue - } - 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].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 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)) - } - } - 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") - } - tagsString := strings.Join(tags, ":") - 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) - fmt.Printf("Encapsulating %s into %s\n", filepath.Base(trackPath), filepath.Base(m4atrackPath)) - if err := cmd.Run(); err != nil { - fmt.Printf("Error encapsulating file: %v\n", err) - continue - } - 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) - 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) - continue - } - } - fmt.Printf("Successfully processed and deleted %s\n", filepath.Base(trackPath)) - oktrackNum += 1 - - } - return err -} - -func main() { - err := loadConfig() - if err != nil { - fmt.Printf("load config failed: %v", err) - return - } - token, err := getToken() - if err != nil { - fmt.Println("Failed to get token.") - return - } - if strings.Contains(os.Args[1], "/artist/") { - newArgs, err := checkArtist(os.Args[1], token) - if err != nil { - fmt.Println("Failed to get artist.") - return - } - os.Args = append([]string{os.Args[0]}, newArgs...) - } - albumTotal := len(os.Args[1:]) - for albumNum, url := range os.Args[1:] { - fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) - var storefront, albumId string - if strings.Contains(url, "/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("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum) -} - -func conventTTMLToLRC(ttml string) (string, error) { - parsedTTML := etree.NewDocument() - err := parsedTTML.ReadFromString(ttml) - if err != nil { - return "", err - } - var lrcLines []string - for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() { - for _, lyric := range item.ChildElements() { - var h, m, s, ms int - if lyric.SelectAttr("begin") == nil { - return "", errors.New("no synchronised lyrics") - } - if strings.Contains(lyric.SelectAttr("begin").Value, ":") { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms) - if err != nil { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms) - h = 0 - } - } else { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms) - h, m = 0, 0 - } - if err != nil { - return "", err - } - var text string - if lyric.SelectAttr("text") == nil { - var textTmp []string - for _, span := range lyric.Child { - if _, ok := span.(*etree.CharData); ok { - textTmp = append(textTmp, span.(*etree.CharData).Data) - } else { - textTmp = append(textTmp, span.(*etree.Element).Text()) - } - } - text = strings.Join(textTmp, "") - } else { - text = lyric.SelectAttr("text").Value - } - m += h * 60 - ms = ms / 10 - lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text)) - } - } - return strings.Join(lrcLines, "\n"), nil -} - -func extractMedia(b string) (string, []string, error) { - masterUrl, err := url.Parse(b) - if err != nil { - return "", nil, err - } - resp, err := http.Get(b) - if err != nil { - return "", nil, err - } - defer resp.Body.Close() - if resp.StatusCode != http.StatusOK { - return "", nil, errors.New(resp.Status) - } - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", nil, err - } - masterString := string(body) - from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) - if err != nil || listType != m3u8.MASTER { - return "", nil, errors.New("m3u8 not of master type") - } - master := from.(*m3u8.MasterPlaylist) - var streamUrl *url.URL - sort.Slice(master.Variants, func(i, j int) bool { - return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth - }) - for _, variant := range master.Variants { - if variant.Codecs == "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 - } - } - } - if streamUrl == nil { - return "", nil, errors.New("no ec-3 codec found") - } - var keys []string - keys = append(keys, prefetchKey) - streamUrl.Path = strings.TrimSuffix(streamUrl.Path, ".m3u8") + "_m.mp4" - regex := regexp.MustCompile(`"(skd?://[^"]*)"`) - matches := regex.FindAllStringSubmatch(masterString, -1) - for _, match := range matches { - if strings.HasSuffix(match[1], "c24") || 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) - } - rawSong, err := ioutil.ReadAll(track.Body) - if err != nil { - return nil, err - } - fmt.Println("Downloaded.") - - f := bytes.NewReader(rawSong) - - trex, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoov(), - mp4.BoxTypeMvex(), - mp4.BoxTypeTrex(), - }) - if err != nil || len(trex) != 1 { - return nil, err - } - trexPay := trex[0].Payload.(*mp4.Trex) - - stbl, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoov(), - mp4.BoxTypeTrak(), - mp4.BoxTypeMdia(), - mp4.BoxTypeMinf(), - mp4.BoxTypeStbl(), - }) - if err != nil || len(stbl) != 1 { - return nil, err - } - - // enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ - // mp4.BoxTypeStsd(), - // mp4.BoxTypeEnca(), - // }) - // if err != nil { - // return nil, err - // } - - // aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, - // []mp4.BoxType{mp4.StrToBoxType("dec3")}) - // if err != nil || len(aalac) != 1 { - // return nil, err - // } - - extracted := &SongInfo{ - r: f, - // alacParam: aalac[0].Payload.(*Alac), - } - - moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoof(), - }) - if err != nil || len(moofs) <= 0 { - return nil, err - } - - mdats, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ - mp4.BoxTypeMdat(), - }) - if err != nil || len(mdats) != len(moofs) { - return nil, err - } - - for i, moof := range moofs { - tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ - mp4.BoxTypeTraf(), - mp4.BoxTypeTfhd(), - }) - if err != nil || len(tfhd) != 1 { - return nil, err - } - tfhdPay := tfhd[0].Payload.(*mp4.Tfhd) - index := tfhdPay.SampleDescriptionIndex - if index != 0 { - index-- - } - - truns, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ - mp4.BoxTypeTraf(), - mp4.BoxTypeTrun(), - }) - if err != nil || len(truns) <= 0 { - return nil, err - } - - mdat := mdats[i].Payload.(*mp4.Mdat).Data - for _, t := range truns { - for _, en := range t.Payload.(*mp4.Trun).Entries { - info := SampleInfo{descIndex: index} - - switch { - case t.Payload.CheckFlag(0x200): - info.data = mdat[:en.SampleSize] - mdat = mdat[en.SampleSize:] - case tfhdPay.CheckFlag(0x10): - info.data = mdat[:tfhdPay.DefaultSampleSize] - mdat = mdat[tfhdPay.DefaultSampleSize:] - default: - info.data = mdat[:trexPay.DefaultSampleSize] - mdat = mdat[trexPay.DefaultSampleSize:] - } - - switch { - case t.Payload.CheckFlag(0x100): - info.duration = en.SampleDuration - case tfhdPay.CheckFlag(0x8): - info.duration = tfhdPay.DefaultSampleDuration - default: - info.duration = trexPay.DefaultSampleDuration - } - - extracted.samples = append(extracted.samples, info) - } - } - if len(mdat) != 0 { - return nil, errors.New("offset mismatch") - } - } - - return extracted, nil -} - -func init() { - mp4.AddBoxDef((*Alac)(nil)) -} - -func BoxTypeAlac() mp4.BoxType { return mp4.StrToBoxType("alac") } - -type Alac struct { - mp4.FullBox `mp4:"extend"` - - FrameLength uint32 `mp4:"size=32"` - CompatibleVersion uint8 `mp4:"size=8"` - BitDepth uint8 `mp4:"size=8"` - Pb uint8 `mp4:"size=8"` - Mb uint8 `mp4:"size=8"` - Kb uint8 `mp4:"size=8"` - NumChannels uint8 `mp4:"size=8"` - MaxRun uint16 `mp4:"size=16"` - MaxFrameBytes uint32 `mp4:"size=32"` - AvgBitRate uint32 `mp4:"size=32"` - SampleRate uint32 `mp4:"size=32"` -} - -func getInfoFromAdam(adamId string, token string, storefront string) (*SongData, error) { - request, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, adamId), nil) - if err != nil { - return nil, err - } - query := url.Values{} - query.Set("extend", "extendedAssetUrls") - query.Set("include", "albums") - request.URL.RawQuery = query.Encode() - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - request.Header.Set("User-Agent", "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)") - request.Header.Set("Origin", "https://music.apple.com") - - do, err := http.DefaultClient.Do(request) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - - obj := new(ApiResult) - err = json.NewDecoder(do.Body).Decode(&obj) - if err != nil { - return nil, err - } - - for _, d := range obj.Data { - if d.ID == adamId { - return &d, nil - } - } - return nil, nil -} - -func getToken() (string, error) { - req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) - if err != nil { - return "", err - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`) - indexJsUri := regex.FindString(string(body)) - - req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil) - if err != nil { - return "", err - } - - resp, err = http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err = io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - regex = regexp.MustCompile(`eyJh([^"]*)`) - token := regex.FindString(string(body)) - - return token, nil -} - -type ApiResult struct { - Data []SongData `json:"data"` -} - -type SongAttributes struct { - ArtistName string `json:"artistName"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - ExtendedAssetUrls struct { - EnhancedHls string `json:"enhancedHls"` - } `json:"extendedAssetUrls"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AlbumName string `json:"albumName"` - TrackNumber int `json:"trackNumber"` - ComposerName string `json:"composerName"` -} - -type AlbumAttributes struct { - ArtistName string `json:"artistName"` - IsSingle bool `json:"isSingle"` - IsComplete bool `json:"isComplete"` - GenreNames []string `json:"genreNames"` - TrackCount int `json:"trackCount"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - RecordLabel string `json:"recordLabel"` - Upc string `json:"upc"` - Copyright string `json:"copyright"` - IsCompilation bool `json:"isCompilation"` -} - -type SongData struct { - ID string `json:"id"` - Attributes SongAttributes `json:"attributes"` - Relationships struct { - Albums struct { - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes AlbumAttributes `json:"attributes"` - } `json:"data"` - } `json:"albums"` - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` -} - -type SongResult struct { - Artwork struct { - Width int `json:"width"` - URL string `json:"url"` - Height int `json:"height"` - TextColor3 string `json:"textColor3"` - TextColor2 string `json:"textColor2"` - TextColor4 string `json:"textColor4"` - HasAlpha bool `json:"hasAlpha"` - TextColor1 string `json:"textColor1"` - BgColor string `json:"bgColor"` - HasP3 bool `json:"hasP3"` - SupportsLayeredImage bool `json:"supportsLayeredImage"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - CollectionID string `json:"collectionId"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - ID string `json:"id"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - ContentRatingsBySystem struct { - } `json:"contentRatingsBySystem"` - Name string `json:"name"` - Composer struct { - Name string `json:"name"` - URL string `json:"url"` - } `json:"composer"` - EditorialArtwork struct { - } `json:"editorialArtwork"` - CollectionName string `json:"collectionName"` - AssetUrls struct { - Plus string `json:"plus"` - Lightweight string `json:"lightweight"` - SuperLightweight string `json:"superLightweight"` - LightweightPlus string `json:"lightweightPlus"` - EnhancedHls string `json:"enhancedHls"` - } `json:"assetUrls"` - AudioTraits []string `json:"audioTraits"` - Kind string `json:"kind"` - Copyright string `json:"copyright"` - ArtistID string `json:"artistId"` - Genres []struct { - GenreID string `json:"genreId"` - Name string `json:"name"` - URL string `json:"url"` - MediaType string `json:"mediaType"` - } `json:"genres"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - Offers []struct { - ActionText struct { - Short string `json:"short"` - Medium string `json:"medium"` - Long string `json:"long"` - Downloaded string `json:"downloaded"` - Downloading string `json:"downloading"` - } `json:"actionText"` - Type string `json:"type"` - PriceFormatted string `json:"priceFormatted"` - Price float64 `json:"price"` - BuyParams string `json:"buyParams"` - Variant string `json:"variant,omitempty"` - Assets []struct { - Flavor string `json:"flavor"` - Preview struct { - Duration int `json:"duration"` - URL string `json:"url"` - } `json:"preview"` - Size int `json:"size"` - Duration int `json:"duration"` - } `json:"assets"` - } `json:"offers"` -} -type iTunesLookup struct { - Results map[string]SongResult `json:"results"` -} - -type Meta struct { - Context string `json:"@context"` - Type string `json:"@type"` - Name string `json:"name"` - Description string `json:"description"` - Tracks []struct { - Type string `json:"@type"` - Name string `json:"name"` - Audio struct { - Type string `json:"@type"` - } `json:"audio"` - Offers struct { - Type string `json:"@type"` - Category string `json:"category"` - Price int `json:"price"` - } `json:"offers"` - Duration string `json:"duration"` - } `json:"tracks"` - Citation []interface{} `json:"citation"` - WorkExample []struct { - Type string `json:"@type"` - Name string `json:"name"` - URL string `json:"url"` - Audio struct { - Type string `json:"@type"` - } `json:"audio"` - Offers struct { - Type string `json:"@type"` - Category string `json:"category"` - Price int `json:"price"` - } `json:"offers"` - Duration string `json:"duration"` - } `json:"workExample"` - Genre []string `json:"genre"` - DatePublished time.Time `json:"datePublished"` - ByArtist struct { - Type string `json:"@type"` - URL string `json:"url"` - Name string `json:"name"` - } `json:"byArtist"` -} - -type AutoGenerated struct { - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - IsSingle bool `json:"isSingle"` - URL string `json:"url"` - IsComplete bool `json:"isComplete"` - GenreNames []string `json:"genreNames"` - TrackCount int `json:"trackCount"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - RecordLabel string `json:"recordLabel"` - Upc string `json:"upc"` - AudioTraits []string `json:"audioTraits"` - Copyright string `json:"copyright"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - IsCompilation bool `json:"isCompilation"` - EditorialVideo struct { - MotionDetailSquare struct { - Video string `json:"video"` - } `json:"motionDetailSquare"` - MotionSquareVideo1x1 struct { - Video string `json:"video"` - } `json:"motionSquareVideo1x1"` - } `json:"editorialVideo"` - } `json:"attributes"` - Relationships struct { - RecordLabels struct { - Href string `json:"href"` - Data []interface{} `json:"data"` - } `json:"record-labels"` - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - Artwork struct { - Url string `json:"url"` - } `json:"artwork"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - Tracks struct { - Href string `json:"href"` - Next string `json:"next"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Previews []struct { - URL string `json:"url"` - } `json:"previews"` - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - URL string `json:"url"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AudioTraits []string `json:"audioTraits"` - HasLyrics bool `json:"hasLyrics"` - AlbumName string `json:"albumName"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - ComposerName string `json:"composerName"` - } `json:"attributes"` - Relationships struct { - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` - } `json:"data"` - } `json:"tracks"` - } `json:"relationships"` - } `json:"data"` -} - -type AutoGeneratedTrack struct { - Href string `json:"href"` - Next string `json:"next"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Previews []struct { - URL string `json:"url"` - } `json:"previews"` - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - URL string `json:"url"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AudioTraits []string `json:"audioTraits"` - HasLyrics bool `json:"hasLyrics"` - AlbumName string `json:"albumName"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - ComposerName string `json:"composerName"` - } `json:"attributes"` - Relationships struct { - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` - } `json:"data"` -} - -type AutoGeneratedArtist struct { - Next string `json:"next"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Previews []struct { - URL string `json:"url"` - } `json:"previews"` - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - URL string `json:"url"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AudioTraits []string `json:"audioTraits"` - HasLyrics bool `json:"hasLyrics"` - AlbumName string `json:"albumName"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - ComposerName string `json:"composerName"` - } `json:"attributes"` - } `json:"data"` -} - -type SongLyrics struct { - Data []struct { - Id string `json:"id"` - Type string `json:"type"` - Attributes struct { - Ttml string `json:"ttml"` - PlayParams struct { - Id string `json:"id"` - Kind string `json:"kind"` - CatalogId string `json:"catalogId"` - DisplayType int `json:"displayType"` - } `json:"playParams"` - } `json:"attributes"` - } `json:"data"` -} diff --git a/main_select.go b/main_select.go deleted file mode 100644 index a3884b2..0000000 --- a/main_select.go +++ /dev/null @@ -1,2390 +0,0 @@ -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/beevik/etree" - "gopkg.in/yaml.v2" - - "github.com/abema/go-mp4" - "github.com/grafov/m3u8" -) - -const ( - defaultId = "0" - prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" -) - -var ( - forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) -) - -type Config struct { - MediaUserToken string `yaml:"media-user-token"` - SaveLrcFile bool `yaml:"save-lrc-file"` - 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"` - DecryptM3u8Port string `yaml:"decrypt-m3u8-port"` - GetM3u8Port string `yaml:"get-m3u8-port"` - AlacMax int `yaml:"alac-max"` - UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` - DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` -} - -var config Config - -type SampleInfo struct { - data []byte - duration uint32 - descIndex uint32 -} - -type SongInfo struct { - r io.ReadSeeker - alacParam *Alac - samples []SampleInfo -} - -func loadConfig() error { - // 读取config.yaml文件内容 - data, err := ioutil.ReadFile("config.yaml") - if err != nil { - return err - } - // 将yaml解析到config变量中 - err = yaml.Unmarshal(data, &config) - if err != nil { - return err - } - return nil -} - -func (s *SongInfo) Duration() (ret uint64) { - for i := range s.samples { - ret += uint64(s.samples[i].duration) - } - return -} - -func (*Alac) GetType() mp4.BoxType { - return BoxTypeAlac() -} - -func fileExists(path string) (bool, error) { - f, err := os.Stat(path) - if err == nil { - return !f.IsDir(), nil - } else if os.IsNotExist(err) { - return false, nil - } - return false, err -} - -func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error { - index := trackNum - 1 - { // ftyp - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()}) - if err != nil { - return err - } - _, err = mp4.Marshal(w, &mp4.Ftyp{ - MajorBrand: [4]byte{'M', '4', 'A', ' '}, - MinorVersion: 0, - CompatibleBrands: []mp4.CompatibleBrandElem{ - {CompatibleBrand: [4]byte{'M', '4', 'A', ' '}}, - {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, - {CompatibleBrand: mp4.BrandISOM()}, - {CompatibleBrand: [4]byte{0, 0, 0, 0}}, - }, - }, box.Context) - if err != nil { - return err - } - _, err = w.EndBox() - if err != nil { - return err - } - } - - const chunkSize uint32 = 5 - duration := info.Duration() - numSamples := uint32(len(info.samples)) - var stco *mp4.BoxInfo - - { // moov - _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()}) - if err != nil { - return err - } - box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()}) - if err != nil { - return err - } - moovOri := box[0] - - { // mvhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()}) - if err != nil { - return err - } - mvhd := oriBox[0].Payload.(*mp4.Mvhd) - if mvhd.Version == 0 { - mvhd.DurationV0 = uint32(duration) - } else { - mvhd.DurationV1 = duration - } - - _, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // trak - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()}) - if err != nil { - return err - } - trakOri := box[0] - - { // tkhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()}) - if err != nil { - return err - } - tkhd := oriBox[0].Payload.(*mp4.Tkhd) - if tkhd.Version == 0 { - tkhd.DurationV0 = uint32(duration) - } else { - tkhd.DurationV1 = duration - } - tkhd.SetFlags(0x7) - - _, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // mdia - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()}) - if err != nil { - return err - } - mdiaOri := box[0] - - { // mdhd - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()}) - if err != nil { - return err - } - - oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()}) - if err != nil { - return err - } - mdhd := oriBox[0].Payload.(*mp4.Mdhd) - if mdhd.Version == 0 { - mdhd.DurationV0 = uint32(duration) - } else { - mdhd.DurationV1 = duration - } - - _, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // hdlr - oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()}) - if err != nil { - return err - } - - err = w.CopyBox(info.r, oriBox[0]) - if err != nil { - return err - } - } - - { // minf - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()}) - if err != nil { - return err - } - - box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()}) - if err != nil { - return err - } - minfOri := box[0] - - { // smhd, dinf - boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{ - {mp4.BoxTypeSmhd()}, - {mp4.BoxTypeDinf()}, - }) - if err != nil { - return err - } - - for _, b := range boxes { - err = w.CopyBox(info.r, b) - if err != nil { - return err - } - } - } - - { // stbl - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()}) - if err != nil { - return err - } - - { // stsd - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()}) - if err != nil { - return err - } - _, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context) - if err != nil { - return err - } - - { // alac - _, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) - if err != nil { - return err - } - - _, err = w.Write([]byte{ - 0, 0, 0, 0, 0, 0, 0, 1, - 0, 0, 0, 0, 0, 0, 0, 0}) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels)) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth)) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0}) - if err != nil { - return err - } - - err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0}) - if err != nil { - return err - } - - box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, info.alacParam, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stts - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()}) - if err != nil { - return err - } - - var stts mp4.Stts - for _, sample := range info.samples { - if len(stts.Entries) != 0 { - last := &stts.Entries[len(stts.Entries)-1] - if last.SampleDelta == sample.duration { - last.SampleCount++ - continue - } - } - stts.Entries = append(stts.Entries, mp4.SttsEntry{ - SampleCount: 1, - SampleDelta: sample.duration, - }) - } - stts.EntryCount = uint32(len(stts.Entries)) - - _, err = mp4.Marshal(w, &stts, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stsc - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()}) - if err != nil { - return err - } - - if numSamples%chunkSize == 0 { - _, err = mp4.Marshal(w, &mp4.Stsc{ - EntryCount: 1, - Entries: []mp4.StscEntry{ - { - FirstChunk: 1, - SamplesPerChunk: chunkSize, - SampleDescriptionIndex: 1, - }, - }, - }, box.Context) - } else { - _, err = mp4.Marshal(w, &mp4.Stsc{ - EntryCount: 2, - Entries: []mp4.StscEntry{ - { - FirstChunk: 1, - SamplesPerChunk: chunkSize, - SampleDescriptionIndex: 1, - }, { - FirstChunk: numSamples/chunkSize + 1, - SamplesPerChunk: numSamples % chunkSize, - SampleDescriptionIndex: 1, - }, - }, - }, box.Context) - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stsz - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()}) - if err != nil { - return err - } - - stsz := mp4.Stsz{SampleCount: numSamples} - for _, sample := range info.samples { - stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data))) - } - - _, err = mp4.Marshal(w, &stsz, box.Context) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // stco - box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()}) - if err != nil { - return err - } - - l := (numSamples + chunkSize - 1) / chunkSize - _, err = mp4.Marshal(w, &mp4.Stco{ - EntryCount: l, - ChunkOffset: make([]uint32, l), - }, box.Context) - - stco, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // udta - ctx := mp4.Context{UnderUdta: true} - _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx}) - if err != nil { - return err - } - - { // meta - ctx.UnderIlstMeta = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, &mp4.Meta{}, ctx) - if err != nil { - return err - } - - { // hdlr - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx}) - if err != nil { - return err - } - - _, err = mp4.Marshal(w, &mp4.Hdlr{ - HandlerType: [4]byte{'m', 'd', 'i', 'r'}, - Reserved: [3]uint32{0x6170706c, 0, 0}, - }, ctx) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { // ilst - ctx.UnderIlst = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx}) - if err != nil { - return err - } - - marshalData := func(val interface{}) error { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) - if err != nil { - return err - } - - var boxData mp4.Data - switch v := val.(type) { - case string: - boxData.DataType = mp4.DataTypeStringUTF8 - boxData.Data = []byte(v) - case uint8: - boxData.DataType = mp4.DataTypeSignedIntBigEndian - boxData.Data = []byte{v} - case uint32: - boxData.DataType = mp4.DataTypeSignedIntBigEndian - boxData.Data = make([]byte, 4) - binary.BigEndian.PutUint32(boxData.Data, v) - case []byte: - boxData.DataType = mp4.DataTypeBinary - boxData.Data = v - default: - panic("unsupported value") - } - - _, err = mp4.Marshal(w, &boxData, ctx) - if err != nil { - return err - } - - _, err = w.EndBox() - return err - } - - addMeta := func(tag mp4.BoxType, val interface{}) error { - _, err = w.StartBox(&mp4.BoxInfo{Type: tag}) - if err != nil { - return err - } - - err = marshalData(val) - if err != nil { - return err - } - - _, err = w.EndBox() - return err - } - - addExtendedMeta := func(name string, val interface{}) error { - ctx.UnderIlstFreeMeta = true - - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx}) - if err != nil { - return err - } - - { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx}) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - - _, err = io.WriteString(w, "com.apple.iTunes") - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - { - _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx}) - if err != nil { - return err - } - - _, err = w.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - - _, err = io.WriteString(w, name) - if err != nil { - return err - } - - _, err = w.EndBox() - if err != nil { - return err - } - } - - err = marshalData(val) - if err != nil { - return err - } - - ctx.UnderIlstFreeMeta = false - - _, err = w.EndBox() - return err - } - - err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'s', 'o', 'n', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) - if err != nil { - return err - } - 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 = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'s', 'o', 'c', 'o'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, meta.Data[0].Attributes.ReleaseDate) - if err != nil { - return err - } - - cnID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].ID, 10, 32) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'c', 'n', 'I', 'D'}, uint32(cnID)) - if err != nil { - return err - } - - err = addExtendedMeta("ISRC", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc) - if err != nil { - return err - } - - if len(meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames) > 0 { - err = addMeta(mp4.BoxType{'\251', 'g', 'e', 'n'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]) - if err != nil { - return err - } - } - - if len(meta.Data) > 0 { - album := meta.Data[0] - - err = addMeta(mp4.BoxType{'a', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) - if err != nil { - return err - } - - err = addMeta(mp4.BoxType{'s', 'o', 'a', 'a'}, meta.Data[0].Relationships.Tracks.Data[index].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("Decrypt start.") - for _, sp := range info.samples { - if lastIndex != sp.descIndex { - if len(decrypted) != 0 { - _, err := conn.Write([]byte{0, 0, 0, 0}) - if err != nil { - return err - } - } - keyUri := keys[sp.descIndex] - id := manifest.Data[0].Relationships.Tracks.Data[trackNum-1].ID - if keyUri == prefetchKey { - id = defaultId - } - - _, err := conn.Write([]byte{byte(len(id))}) - if err != nil { - return err - } - _, err = io.WriteString(conn, id) - if err != nil { - return err - } - - _, err = conn.Write([]byte{byte(len(keyUri))}) - if err != nil { - return err - } - _, err = io.WriteString(conn, keyUri) - if err != nil { - return err - } - } - lastIndex = sp.descIndex - - err := binary.Write(conn, binary.LittleEndian, uint32(len(sp.data))) - if err != nil { - return err - } - - _, err = conn.Write(sp.data) - if err != nil { - return err - } - - de := make([]byte, len(sp.data)) - _, err = io.ReadFull(conn, de) - if err != nil { - return err - } - - decrypted = append(decrypted, de...) - } - _, _ = conn.Write([]byte{0, 0, 0, 0, 0}) - - fmt.Println("Decrypt finished.") - - create, err := os.Create(filename) - if err != nil { - return err - } - defer create.Close() - - return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal) -} - -func checkUrl(url string) (string, string) { - pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) - matches := pat.FindAllStringSubmatch(url, -1) - - if matches == nil { - return "", "" - } else { - return matches[0][1], matches[0][2] - } -} -func checkUrlPlaylist(url string) (string, string) { - pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/playlist|\/playlist\/.+))\/(?:id)?(pl\.[\w-]+)(?:$|\?)`) - matches := pat.FindAllStringSubmatch(url, -1) - - if matches == nil { - return "", "" - } else { - return matches[0][1], matches[0][2] - } -} - -func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { - var mtype string - var page int - if strings.Contains(albumId, "pl.") { - mtype = "playlists" - } else { - mtype = "albums" - } - req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s", storefront, mtype, albumId), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Origin", "https://music.apple.com") - query := url.Values{} - query.Set("omit[resource]", "autos") - query.Set("include", "tracks,artists,record-labels") - query.Set("include[songs]", "artists") - query.Set("fields[artists]", "name,artwork") - query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url") - query.Set("fields[record-labels]", "name") - query.Set("extend", "editorialVideo") - // query.Set("l", "en-gb") - req.URL.RawQuery = query.Encode() - do, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - obj := new(AutoGenerated) - err = json.NewDecoder(do.Body).Decode(&obj) - if err != nil { - return nil, err - } - if strings.Contains(albumId, "pl.") { - obj.Data[0].Attributes.ArtistName = "Apple Music" - if len(obj.Data[0].Relationships.Tracks.Next) > 0 { - page = 0 - for { - page = page + 100 - pageStr := strconv.Itoa(page) - req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/%s/%s/tracks?offset=%s", storefront, mtype, albumId, pageStr), nil) - if err != nil { - return nil, err - } - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - req.Header.Set("Origin", "https://music.apple.com") - do, err := http.DefaultClient.Do(req) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - obj2 := new(AutoGeneratedTrack) - err = json.NewDecoder(do.Body).Decode(&obj2) - if err != nil { - return nil, err - } - for _, value := range obj2.Data { - obj.Data[0].Relationships.Tracks.Data = append(obj.Data[0].Relationships.Tracks.Data, value) - } - if len(obj2.Next) == 0 { - break - } - } - } - } - return obj, nil -} - -func getSongLyrics(songId string, storefront string, token string, userToken string) (string, error) { - req, err := http.NewRequest("GET", - fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s/lyrics", storefront, songId), nil) - if err != nil { - return "", err - } - req.Header.Set("Origin", "https://music.apple.com") - req.Header.Set("Referer", "https://music.apple.com/") - req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - cookie := http.Cookie{Name: "media-user-token", Value: userToken} - req.AddCookie(&cookie) - do, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer do.Body.Close() - obj := new(SongLyrics) - err = json.NewDecoder(do.Body).Decode(&obj) - if obj.Data != nil { - return obj.Data[0].Attributes.Ttml, nil - } else { - return "", errors.New("failed to get lyrics") - } -} - -func writeCover(sanAlbumFolder, name string, url string) error { - covPath := filepath.Join(sanAlbumFolder, name+"."+config.CoverFormat) - exists, err := fileExists(covPath) - if err != nil { - fmt.Println("Failed to check if cover exists.") - return err - } - if exists { - return nil - } - if config.CoverFormat == "png" { - re := regexp.MustCompile(`\{w\}x\{h\}`) - parts := re.Split(url, 2) - url = parts[0] + "{w}x{h}" + strings.Replace(parts[1], ".jpg", ".png", 1) - } - url = strings.Replace(url, "{w}x{h}", config.CoverSize, 1) - req, err := http.NewRequest("GET", url, nil) - if err != nil { - return err - } - req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") - do, err := http.DefaultClient.Do(req) - if err != nil { - return err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - errors.New(do.Status) - } - f, err := os.Create(covPath) - if err != nil { - return err - } - defer f.Close() - _, err = io.Copy(f, do.Body) - if err != nil { - return err - } - return nil -} - -func writeLyrics(sanAlbumFolder, filename string, lrc string) error { - lyricspath := filepath.Join(sanAlbumFolder, filename) - f, err := os.Create(lyricspath) - if err != nil { - return err - } - defer f.Close() - _, err = f.WriteString(lrc) - if err != nil { - return err - } - return nil -} - -func isInArray(arr []int, target int) bool { - for _, num := range arr { - if num == target { - return true - } - } - return false -} - -func rip(albumId string, token string, storefront string, userToken string) error { - - meta, err := getMeta(albumId, token, storefront) - if err != nil { - fmt.Println("Failed to get album metadata.\n") - return err - } - var singerFoldername string - if config.ArtistFolderFormat != "" { - if strings.Contains(albumId, "pl.") { - singerFoldername = strings.NewReplacer( - "{ArtistName}", "Apple Music", - "{ArtistId}", "", - ).Replace(config.ArtistFolderFormat) - } else if len(meta.Data[0].Relationships.Artists.Data) > 0 { - singerFoldername = strings.NewReplacer( - "{ArtistName}", meta.Data[0].Attributes.ArtistName, - "{ArtistId}", meta.Data[0].Relationships.Artists.Data[0].ID, - ).Replace(config.ArtistFolderFormat) - } else { - singerFoldername = strings.NewReplacer( - "{ArtistName}", 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, "_")) - 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 Quality string - if strings.Contains(config.AlbumFolderFormat, "Quality") { - 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 in ALAC.\n") - } else { - Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) - if err != nil { - fmt.Println("Failed to extract quality from manifest.\n", err) - } - } - } - } - var albumFolder string - if strings.Contains(albumId, "pl.") { - albumFolder = strings.NewReplacer( - "{ArtistName}", "Apple Music", - "{PlaylistName}", meta.Data[0].Attributes.Name, - "{PlaylistId}", albumId, - "{Quality}", Quality, - "{Codec}", "ALAC", - "{Tag}", Tag_string, - ).Replace(config.PlaylistFolderFormat) - } else { - albumFolder = strings.NewReplacer( - "{ReleaseDate}", meta.Data[0].Attributes.ReleaseDate, - "{ReleaseYear}", meta.Data[0].Attributes.ReleaseDate[:4], - "{ArtistName}", meta.Data[0].Attributes.ArtistName, - "{AlbumName}", meta.Data[0].Attributes.Name, - "{UPC}", meta.Data[0].Attributes.Upc, - "{Copyright}", meta.Data[0].Attributes.Copyright, - "{AlbumId}", albumId, - "{Quality}", Quality, - "{Codec}", "ALAC", - "{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 - } - fmt.Print("select: ") - reader := bufio.NewReader(os.Stdin) - input, err := reader.ReadString('\n') - if err != nil { - fmt.Println(err) - } - manually := false - manually_txt := false - m3u8_txt := "" - if strings.Contains(input, "txt") { - m3u8_txt = strings.TrimSpace(input) - fmt.Print(m3u8_txt) - strArr := make([]string, len(arr)) - for i, num := range arr { - strArr[i] = strconv.Itoa(num) - } - input = strings.Join(strArr, " ") - manually_txt = true - } - if strings.Contains(input, "#") { - input = strings.ReplaceAll(input, "#", "") - manually = true - } - 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) - } - - for i := 0; i < len(arr); i++ { - if arr[i] == num { - arr = append(arr[:i], arr[i+1:]...) - break - } - } - } - for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { - trackNum++ - if isInArray(arr, trackNum) { - continue - } else { - fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) - manifest, err := getInfoFromAdam(track.ID, token, storefront) - if err != nil { - fmt.Println("Failed to get manifest.\n", err) - continue - } - if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { - fmt.Println("Unavailable in ALAC.") - continue - } - var Quality string - if strings.Contains(config.SongFileFormat, "Quality") { - Quality, err = extractMediaQuality(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) - if err != nil { - fmt.Println("Failed to extract quality from manifest.\n", err) - 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}", track.Attributes.Name, - "{DiscNumber}", fmt.Sprintf("%0d", track.Attributes.DiscNumber), - "{TrackNumber}", fmt.Sprintf("%0d", track.Attributes.TrackNumber), - "{Quality}", Quality, - "{Codec}", "ALAC", - "{Tag}", Tag_string, - ).Replace(config.SongFileFormat) - fmt.Println(songName) - filename := fmt.Sprintf("%s.m4a", forbiddenNames.ReplaceAllString(songName, "_")) - lrcFilename := fmt.Sprintf("%s.lrc", forbiddenNames.ReplaceAllString(songName, "_")) - trackPath := filepath.Join(sanAlbumFolder, filename) - var lrc string = "" - if userToken != "your-media-user-token" && (config.EmbedLrc || config.SaveLrcFile) { - ttml, err := getSongLyrics(track.ID, storefront, token, userToken) - if err != nil { - fmt.Println("Failed to get lyrics") - } else { - lrc, err = conventTTMLToLRC(ttml) - if err != nil { - fmt.Printf("Failed to parse lyrics: %s \n", err) - } else { - if config.SaveLrcFile { - err := writeLyrics(sanAlbumFolder, lrcFilename, lrc) - if err != nil { - fmt.Printf("Failed to write lyrics") - } - if !config.EmbedLrc { - lrc = "" - } - } - } - } - } - exists, err := fileExists(trackPath) - if err != nil { - fmt.Println("Failed to check if track exists.") - } - if exists { - fmt.Println("Track already exists locally.") - continue - } - if manually_txt { - file, err := os.Open(m3u8_txt) - if err != nil { - fmt.Println("cant open txt:", err) - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := scanner.Text() - if strings.HasPrefix(line, track.ID) { - parts := strings.SplitN(line, ",", 2) - if len(parts) == 2 { - manifest.Attributes.ExtendedAssetUrls.EnhancedHls = parts[1] - } - } - } - if err := scanner.Err(); err != nil { - fmt.Println(err) - } - } - if manually { - fmt.Print("m3u8: ") - reader := bufio.NewReader(os.Stdin) - m3u8_url, err := reader.ReadString('\n') - if err != nil { - fmt.Println(err) - } - m3u8_url = strings.TrimSpace(m3u8_url) - manifest.Attributes.ExtendedAssetUrls.EnhancedHls = m3u8_url - } - trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) - if err != nil { - fmt.Println("Failed to extract info from manifest.\n", err) - continue - } - info, err := extractSong(trackUrl) - if err != nil { - fmt.Println("Failed to extract track.", err) - continue - } - samplesOk := true - for samplesOk { - for _, i := range info.samples { - if int(i.descIndex) >= len(keys) { - fmt.Println("Decryption size mismatch.") - samplesOk = false - } - } - break - } - if !samplesOk { - continue - } - err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) - if err != nil { - fmt.Println("Failed to decrypt track.\n", err) - continue - } - tags := []string{ - fmt.Sprintf("lyrics=%s", lrc), - } - if config.EmbedCover { - 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)) - } - } - - 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") - } - tagsString := strings.Join(tags, ":") - cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath) - if err := cmd.Run(); err != nil { - fmt.Printf("Embed failed: %v\n", err) - continue - } - 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) - continue - } - } - } - } - return err -} - -func main() { - err := loadConfig() - if err != nil { - fmt.Printf("load config failed: %v", err) - return - } - token, err := getToken() - if err != nil { - fmt.Println("Failed to get token.") - return - } - albumTotal := len(os.Args[1:]) - for albumNum, url := range os.Args[1:] { - fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) - var storefront, albumId string - if strings.Contains(url, "/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) - } - } -} - -func conventTTMLToLRC(ttml string) (string, error) { - parsedTTML := etree.NewDocument() - err := parsedTTML.ReadFromString(ttml) - if err != nil { - return "", err - } - var lrcLines []string - for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() { - for _, lyric := range item.ChildElements() { - var h, m, s, ms int - if lyric.SelectAttr("begin") == nil { - return "", errors.New("no synchronised lyrics") - } - if strings.Contains(lyric.SelectAttr("begin").Value, ":") { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms) - if err != nil { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms) - h = 0 - } - } else { - _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms) - h, m = 0, 0 - } - if err != nil { - return "", err - } - var text string - if lyric.SelectAttr("text") == nil { - var textTmp []string - for _, span := range lyric.Child { - if _, ok := span.(*etree.CharData); ok { - textTmp = append(textTmp, span.(*etree.CharData).Data) - } else { - textTmp = append(textTmp, span.(*etree.Element).Text()) - } - } - text = strings.Join(textTmp, "") - } else { - text = lyric.SelectAttr("text").Value - } - m += h * 60 - ms = ms / 10 - lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text)) - } - } - return strings.Join(lrcLines, "\n"), nil -} -func 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 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 alac codec found") - } - var keys []string - keys = append(keys, prefetchKey) - streamUrl.Path = strings.TrimSuffix(streamUrl.Path, ".m3u8") + "_m.mp4" - regex := regexp.MustCompile(`"(skd?://[^"]*)"`) - matches := regex.FindAllStringSubmatch(masterString, -1) - for _, match := range matches { - if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") { - keys = append(keys, match[1]) - } - } - return streamUrl.String(), keys, nil -} -func 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) - } - rawSong, err := ioutil.ReadAll(track.Body) - if err != nil { - return nil, err - } - fmt.Println("Downloaded.") - f := bytes.NewReader(rawSong) - - trex, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoov(), - mp4.BoxTypeMvex(), - mp4.BoxTypeTrex(), - }) - if err != nil || len(trex) != 1 { - return nil, err - } - trexPay := trex[0].Payload.(*mp4.Trex) - - stbl, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoov(), - mp4.BoxTypeTrak(), - mp4.BoxTypeMdia(), - mp4.BoxTypeMinf(), - mp4.BoxTypeStbl(), - }) - if err != nil || len(stbl) != 1 { - return nil, err - } - - enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ - mp4.BoxTypeStsd(), - mp4.BoxTypeEnca(), - }) - if err != nil { - return nil, err - } - - aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, - []mp4.BoxType{BoxTypeAlac()}) - if err != nil || len(aalac) != 1 { - return nil, err - } - - extracted := &SongInfo{ - r: f, - alacParam: aalac[0].Payload.(*Alac), - } - - moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ - mp4.BoxTypeMoof(), - }) - if err != nil || len(moofs) <= 0 { - return nil, err - } - - mdats, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ - mp4.BoxTypeMdat(), - }) - if err != nil || len(mdats) != len(moofs) { - return nil, err - } - - for i, moof := range moofs { - tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ - mp4.BoxTypeTraf(), - mp4.BoxTypeTfhd(), - }) - if err != nil || len(tfhd) != 1 { - return nil, err - } - tfhdPay := tfhd[0].Payload.(*mp4.Tfhd) - index := tfhdPay.SampleDescriptionIndex - if index != 0 { - index-- - } - - truns, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ - mp4.BoxTypeTraf(), - mp4.BoxTypeTrun(), - }) - if err != nil || len(truns) <= 0 { - return nil, err - } - - mdat := mdats[i].Payload.(*mp4.Mdat).Data - for _, t := range truns { - for _, en := range t.Payload.(*mp4.Trun).Entries { - info := SampleInfo{descIndex: index} - - switch { - case t.Payload.CheckFlag(0x200): - info.data = mdat[:en.SampleSize] - mdat = mdat[en.SampleSize:] - case tfhdPay.CheckFlag(0x10): - info.data = mdat[:tfhdPay.DefaultSampleSize] - mdat = mdat[tfhdPay.DefaultSampleSize:] - default: - info.data = mdat[:trexPay.DefaultSampleSize] - mdat = mdat[trexPay.DefaultSampleSize:] - } - - switch { - case t.Payload.CheckFlag(0x100): - info.duration = en.SampleDuration - case tfhdPay.CheckFlag(0x8): - info.duration = tfhdPay.DefaultSampleDuration - default: - info.duration = trexPay.DefaultSampleDuration - } - - extracted.samples = append(extracted.samples, info) - } - } - if len(mdat) != 0 { - return nil, errors.New("offset mismatch") - } - } - - return extracted, nil -} - -func init() { - mp4.AddBoxDef((*Alac)(nil)) -} - -func BoxTypeAlac() mp4.BoxType { return mp4.StrToBoxType("alac") } - -type Alac struct { - mp4.FullBox `mp4:"extend"` - - FrameLength uint32 `mp4:"size=32"` - CompatibleVersion uint8 `mp4:"size=8"` - BitDepth uint8 `mp4:"size=8"` - Pb uint8 `mp4:"size=8"` - Mb uint8 `mp4:"size=8"` - Kb uint8 `mp4:"size=8"` - NumChannels uint8 `mp4:"size=8"` - MaxRun uint16 `mp4:"size=16"` - MaxFrameBytes uint32 `mp4:"size=32"` - AvgBitRate uint32 `mp4:"size=32"` - SampleRate uint32 `mp4:"size=32"` -} - -func getInfoFromAdam(adamId string, token string, storefront string) (*SongData, error) { - request, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, adamId), nil) - if err != nil { - return nil, err - } - query := url.Values{} - query.Set("extend", "extendedAssetUrls") - query.Set("include", "albums") - request.URL.RawQuery = query.Encode() - - request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) - request.Header.Set("User-Agent", "iTunes/12.11.3 (Windows; Microsoft Windows 10 x64 Professional Edition (Build 19041); x64) AppleWebKit/7611.1022.4001.1 (dt:2)") - request.Header.Set("Origin", "https://music.apple.com") - - do, err := http.DefaultClient.Do(request) - if err != nil { - return nil, err - } - defer do.Body.Close() - if do.StatusCode != http.StatusOK { - return nil, errors.New(do.Status) - } - - obj := new(ApiResult) - err = json.NewDecoder(do.Body).Decode(&obj) - if err != nil { - return nil, err - } - - for _, d := range obj.Data { - if d.ID == adamId { - return &d, nil - } - } - return nil, nil -} - -func getToken() (string, error) { - req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) - if err != nil { - return "", err - } - - resp, err := http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err := io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`) - indexJsUri := regex.FindString(string(body)) - - req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil) - if err != nil { - return "", err - } - - resp, err = http.DefaultClient.Do(req) - if err != nil { - return "", err - } - defer resp.Body.Close() - - body, err = io.ReadAll(resp.Body) - if err != nil { - return "", err - } - - regex = regexp.MustCompile(`eyJh([^"]*)`) - token := regex.FindString(string(body)) - - return token, nil -} - -type ApiResult struct { - Data []SongData `json:"data"` -} - -type SongAttributes struct { - ArtistName string `json:"artistName"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - ExtendedAssetUrls struct { - EnhancedHls string `json:"enhancedHls"` - } `json:"extendedAssetUrls"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AlbumName string `json:"albumName"` - TrackNumber int `json:"trackNumber"` - ComposerName string `json:"composerName"` -} - -type AlbumAttributes struct { - ArtistName string `json:"artistName"` - IsSingle bool `json:"isSingle"` - IsComplete bool `json:"isComplete"` - GenreNames []string `json:"genreNames"` - TrackCount int `json:"trackCount"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - RecordLabel string `json:"recordLabel"` - Upc string `json:"upc"` - Copyright string `json:"copyright"` - IsCompilation bool `json:"isCompilation"` -} - -type SongData struct { - ID string `json:"id"` - Attributes SongAttributes `json:"attributes"` - Relationships struct { - Albums struct { - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes AlbumAttributes `json:"attributes"` - } `json:"data"` - } `json:"albums"` - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` -} - -type SongResult struct { - Artwork struct { - Width int `json:"width"` - URL string `json:"url"` - Height int `json:"height"` - TextColor3 string `json:"textColor3"` - TextColor2 string `json:"textColor2"` - TextColor4 string `json:"textColor4"` - HasAlpha bool `json:"hasAlpha"` - TextColor1 string `json:"textColor1"` - BgColor string `json:"bgColor"` - HasP3 bool `json:"hasP3"` - SupportsLayeredImage bool `json:"supportsLayeredImage"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - CollectionID string `json:"collectionId"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - ID string `json:"id"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - ContentRatingsBySystem struct { - } `json:"contentRatingsBySystem"` - Name string `json:"name"` - Composer struct { - Name string `json:"name"` - URL string `json:"url"` - } `json:"composer"` - EditorialArtwork struct { - } `json:"editorialArtwork"` - CollectionName string `json:"collectionName"` - AssetUrls struct { - Plus string `json:"plus"` - Lightweight string `json:"lightweight"` - SuperLightweight string `json:"superLightweight"` - LightweightPlus string `json:"lightweightPlus"` - EnhancedHls string `json:"enhancedHls"` - } `json:"assetUrls"` - AudioTraits []string `json:"audioTraits"` - Kind string `json:"kind"` - Copyright string `json:"copyright"` - ArtistID string `json:"artistId"` - Genres []struct { - GenreID string `json:"genreId"` - Name string `json:"name"` - URL string `json:"url"` - MediaType string `json:"mediaType"` - } `json:"genres"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - Offers []struct { - ActionText struct { - Short string `json:"short"` - Medium string `json:"medium"` - Long string `json:"long"` - Downloaded string `json:"downloaded"` - Downloading string `json:"downloading"` - } `json:"actionText"` - Type string `json:"type"` - PriceFormatted string `json:"priceFormatted"` - Price float64 `json:"price"` - BuyParams string `json:"buyParams"` - Variant string `json:"variant,omitempty"` - Assets []struct { - Flavor string `json:"flavor"` - Preview struct { - Duration int `json:"duration"` - URL string `json:"url"` - } `json:"preview"` - Size int `json:"size"` - Duration int `json:"duration"` - } `json:"assets"` - } `json:"offers"` -} -type iTunesLookup struct { - Results map[string]SongResult `json:"results"` -} - -type Meta struct { - Context string `json:"@context"` - Type string `json:"@type"` - Name string `json:"name"` - Description string `json:"description"` - Tracks []struct { - Type string `json:"@type"` - Name string `json:"name"` - Audio struct { - Type string `json:"@type"` - } `json:"audio"` - Offers struct { - Type string `json:"@type"` - Category string `json:"category"` - Price int `json:"price"` - } `json:"offers"` - Duration string `json:"duration"` - } `json:"tracks"` - Citation []interface{} `json:"citation"` - WorkExample []struct { - Type string `json:"@type"` - Name string `json:"name"` - URL string `json:"url"` - Audio struct { - Type string `json:"@type"` - } `json:"audio"` - Offers struct { - Type string `json:"@type"` - Category string `json:"category"` - Price int `json:"price"` - } `json:"offers"` - Duration string `json:"duration"` - } `json:"workExample"` - Genre []string `json:"genre"` - DatePublished time.Time `json:"datePublished"` - ByArtist struct { - Type string `json:"@type"` - URL string `json:"url"` - Name string `json:"name"` - } `json:"byArtist"` -} - -type AutoGenerated struct { - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - IsSingle bool `json:"isSingle"` - URL string `json:"url"` - IsComplete bool `json:"isComplete"` - GenreNames []string `json:"genreNames"` - TrackCount int `json:"trackCount"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - RecordLabel string `json:"recordLabel"` - Upc string `json:"upc"` - AudioTraits []string `json:"audioTraits"` - Copyright string `json:"copyright"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - IsCompilation bool `json:"isCompilation"` - EditorialVideo struct { - MotionDetailSquare struct { - Video string `json:"video"` - } `json:"motionDetailSquare"` - MotionSquareVideo1x1 struct { - Video string `json:"video"` - } `json:"motionSquareVideo1x1"` - } `json:"editorialVideo"` - } `json:"attributes"` - Relationships struct { - RecordLabels struct { - Href string `json:"href"` - Data []interface{} `json:"data"` - } `json:"record-labels"` - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - Artwork struct { - Url string `json:"url"` - } `json:"artwork"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - Tracks struct { - Href string `json:"href"` - Next string `json:"next"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Previews []struct { - URL string `json:"url"` - } `json:"previews"` - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - URL string `json:"url"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AudioTraits []string `json:"audioTraits"` - HasLyrics bool `json:"hasLyrics"` - AlbumName string `json:"albumName"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - ComposerName string `json:"composerName"` - } `json:"attributes"` - Relationships struct { - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` - } `json:"data"` - } `json:"tracks"` - } `json:"relationships"` - } `json:"data"` -} - -type AutoGeneratedTrack struct { - Href string `json:"href"` - Next string `json:"next"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Previews []struct { - URL string `json:"url"` - } `json:"previews"` - Artwork struct { - Width int `json:"width"` - Height int `json:"height"` - URL string `json:"url"` - BgColor string `json:"bgColor"` - TextColor1 string `json:"textColor1"` - TextColor2 string `json:"textColor2"` - TextColor3 string `json:"textColor3"` - TextColor4 string `json:"textColor4"` - } `json:"artwork"` - ArtistName string `json:"artistName"` - URL string `json:"url"` - DiscNumber int `json:"discNumber"` - GenreNames []string `json:"genreNames"` - HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` - IsMasteredForItunes bool `json:"isMasteredForItunes"` - IsAppleDigitalMaster bool `json:"isAppleDigitalMaster"` - ContentRating string `json:"contentRating"` - DurationInMillis int `json:"durationInMillis"` - ReleaseDate string `json:"releaseDate"` - Name string `json:"name"` - Isrc string `json:"isrc"` - AudioTraits []string `json:"audioTraits"` - HasLyrics bool `json:"hasLyrics"` - AlbumName string `json:"albumName"` - PlayParams struct { - ID string `json:"id"` - Kind string `json:"kind"` - } `json:"playParams"` - TrackNumber int `json:"trackNumber"` - AudioLocale string `json:"audioLocale"` - ComposerName string `json:"composerName"` - } `json:"attributes"` - Relationships struct { - Artists struct { - Href string `json:"href"` - Data []struct { - ID string `json:"id"` - Type string `json:"type"` - Href string `json:"href"` - Attributes struct { - Name string `json:"name"` - } `json:"attributes"` - } `json:"data"` - } `json:"artists"` - } `json:"relationships"` - } `json:"data"` -} - -type 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"` -}