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" "gopkg.in/yaml.v2" "github.com/abema/go-mp4" "github.com/beevik/etree" "github.com/grafov/m3u8" ) const ( defaultId = "0" prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" ) var ( forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) ) 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"` ForceApi bool `yaml:"force-api"` Check string `yaml:"check"` DecryptM3u8Port string `yaml:"decrypt-m3u8-port"` GetM3u8Port string `yaml:"get-m3u8-port"` GetM3u8FromDevice bool `yaml:"get-m3u8-from-device"` AlacMax int `yaml:"alac-max"` UseSongInfoForPlaylist bool `yaml:"use-songinfo-for-playlist"` DlAlbumcoverForPlaylist bool `yaml:"dl-albumcover-for-playlist"` } var config Config var txtpath string var oktrackNum int = 0 var trackTotalnum int = 0 type SampleInfo struct { data []byte duration uint32 descIndex uint32 } type SongInfo struct { r io.ReadSeeker alacParam *Alac samples []SampleInfo } func loadConfig() error { // 读取config.yaml文件内容 data, err := ioutil.ReadFile("config.yaml") if err != nil { return err } // 将yaml解析到config变量中 err = yaml.Unmarshal(data, &config) if err != nil { return err } return nil } func (s *SongInfo) Duration() (ret uint64) { for i := range s.samples { ret += uint64(s.samples[i].duration) } return } func (*Alac) GetType() mp4.BoxType { return BoxTypeAlac() } func fileExists(path string) (bool, error) { f, err := os.Stat(path) if err == nil { return !f.IsDir(), nil } else if os.IsNotExist(err) { return false, nil } return false, err } func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error { index := trackNum - 1 { // ftyp box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()}) if err != nil { return err } _, err = mp4.Marshal(w, &mp4.Ftyp{ MajorBrand: [4]byte{'M', '4', 'A', ' '}, MinorVersion: 0, CompatibleBrands: []mp4.CompatibleBrandElem{ {CompatibleBrand: [4]byte{'M', '4', 'A', ' '}}, {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, {CompatibleBrand: mp4.BrandISOM()}, {CompatibleBrand: [4]byte{0, 0, 0, 0}}, }, }, box.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } const chunkSize uint32 = 5 duration := info.Duration() numSamples := uint32(len(info.samples)) var stco *mp4.BoxInfo { // moov _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()}) if err != nil { return err } box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()}) if err != nil { return err } moovOri := box[0] { // mvhd _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()}) if err != nil { return err } oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()}) if err != nil { return err } mvhd := oriBox[0].Payload.(*mp4.Mvhd) if mvhd.Version == 0 { mvhd.DurationV0 = uint32(duration) } else { mvhd.DurationV1 = duration } _, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // trak _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()}) if err != nil { return err } box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()}) if err != nil { return err } trakOri := box[0] { // tkhd _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()}) if err != nil { return err } oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()}) if err != nil { return err } tkhd := oriBox[0].Payload.(*mp4.Tkhd) if tkhd.Version == 0 { tkhd.DurationV0 = uint32(duration) } else { tkhd.DurationV1 = duration } tkhd.SetFlags(0x7) _, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // mdia _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()}) if err != nil { return err } box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()}) if err != nil { return err } mdiaOri := box[0] { // mdhd _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()}) if err != nil { return err } oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()}) if err != nil { return err } mdhd := oriBox[0].Payload.(*mp4.Mdhd) if mdhd.Version == 0 { mdhd.DurationV0 = uint32(duration) } else { mdhd.DurationV1 = duration } _, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // hdlr oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()}) if err != nil { return err } err = w.CopyBox(info.r, oriBox[0]) if err != nil { return err } } { // minf _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()}) if err != nil { return err } box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()}) if err != nil { return err } minfOri := box[0] { // smhd, dinf boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{ {mp4.BoxTypeSmhd()}, {mp4.BoxTypeDinf()}, }) if err != nil { return err } for _, b := range boxes { err = w.CopyBox(info.r, b) if err != nil { return err } } } { // stbl _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()}) if err != nil { return err } { // stsd box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()}) if err != nil { return err } _, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context) if err != nil { return err } { // alac _, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) if err != nil { return err } _, err = w.Write([]byte{ 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0}) if err != nil { return err } err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels)) if err != nil { return err } err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth)) if err != nil { return err } _, err = w.Write([]byte{0, 0}) if err != nil { return err } err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate) if err != nil { return err } _, err = w.Write([]byte{0, 0}) if err != nil { return err } box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) if err != nil { return err } _, err = mp4.Marshal(w, info.alacParam, box.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } _, err = w.EndBox() if err != nil { return err } } { // stts box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()}) if err != nil { return err } var stts mp4.Stts for _, sample := range info.samples { if len(stts.Entries) != 0 { last := &stts.Entries[len(stts.Entries)-1] if last.SampleDelta == sample.duration { last.SampleCount++ continue } } stts.Entries = append(stts.Entries, mp4.SttsEntry{ SampleCount: 1, SampleDelta: sample.duration, }) } stts.EntryCount = uint32(len(stts.Entries)) _, err = mp4.Marshal(w, &stts, box.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // stsc box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()}) if err != nil { return err } if numSamples%chunkSize == 0 { _, err = mp4.Marshal(w, &mp4.Stsc{ EntryCount: 1, Entries: []mp4.StscEntry{ { FirstChunk: 1, SamplesPerChunk: chunkSize, SampleDescriptionIndex: 1, }, }, }, box.Context) } else { _, err = mp4.Marshal(w, &mp4.Stsc{ EntryCount: 2, Entries: []mp4.StscEntry{ { FirstChunk: 1, SamplesPerChunk: chunkSize, SampleDescriptionIndex: 1, }, { FirstChunk: numSamples/chunkSize + 1, SamplesPerChunk: numSamples % chunkSize, SampleDescriptionIndex: 1, }, }, }, box.Context) } _, err = w.EndBox() if err != nil { return err } } { // stsz box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()}) if err != nil { return err } stsz := mp4.Stsz{SampleCount: numSamples} for _, sample := range info.samples { stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data))) } _, err = mp4.Marshal(w, &stsz, box.Context) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // stco box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()}) if err != nil { return err } l := (numSamples + chunkSize - 1) / chunkSize _, err = mp4.Marshal(w, &mp4.Stco{ EntryCount: l, ChunkOffset: make([]uint32, l), }, box.Context) stco, err = w.EndBox() if err != nil { return err } } _, err = w.EndBox() if err != nil { return err } } _, err = w.EndBox() if err != nil { return err } } _, err = w.EndBox() if err != nil { return err } } _, err = w.EndBox() if err != nil { return err } } { // udta ctx := mp4.Context{UnderUdta: true} _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx}) if err != nil { return err } { // meta ctx.UnderIlstMeta = true _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx}) if err != nil { return err } _, err = mp4.Marshal(w, &mp4.Meta{}, ctx) if err != nil { return err } { // hdlr _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx}) if err != nil { return err } _, err = mp4.Marshal(w, &mp4.Hdlr{ HandlerType: [4]byte{'m', 'd', 'i', 'r'}, Reserved: [3]uint32{0x6170706c, 0, 0}, }, ctx) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { // ilst ctx.UnderIlst = true _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx}) if err != nil { return err } marshalData := func(val interface{}) error { _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) if err != nil { return err } var boxData mp4.Data switch v := val.(type) { case string: boxData.DataType = mp4.DataTypeStringUTF8 boxData.Data = []byte(v) case uint8: boxData.DataType = mp4.DataTypeSignedIntBigEndian boxData.Data = []byte{v} case uint32: boxData.DataType = mp4.DataTypeSignedIntBigEndian boxData.Data = make([]byte, 4) binary.BigEndian.PutUint32(boxData.Data, v) case []byte: boxData.DataType = mp4.DataTypeBinary boxData.Data = v default: panic("unsupported value") } _, err = mp4.Marshal(w, &boxData, ctx) if err != nil { return err } _, err = w.EndBox() return err } addMeta := func(tag mp4.BoxType, val interface{}) error { _, err = w.StartBox(&mp4.BoxInfo{Type: tag}) if err != nil { return err } err = marshalData(val) if err != nil { return err } _, err = w.EndBox() return err } addExtendedMeta := func(name string, val interface{}) error { ctx.UnderIlstFreeMeta = true _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx}) if err != nil { return err } { _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx}) if err != nil { return err } _, err = w.Write([]byte{0, 0, 0, 0}) if err != nil { return err } _, err = io.WriteString(w, "com.apple.iTunes") if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } { _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx}) if err != nil { return err } _, err = w.Write([]byte{0, 0, 0, 0}) if err != nil { return err } _, err = io.WriteString(w, name) if err != nil { return err } _, err = w.EndBox() if err != nil { return err } } err = marshalData(val) if err != nil { return err } ctx.UnderIlstFreeMeta = false _, err = w.EndBox() return err } err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) if err != nil { return err } err = addMeta(mp4.BoxType{'s', 'o', 'n', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) if err != nil { return err } AlbumName := meta.Data[0].Relationships.Tracks.Data[index].Attributes.AlbumName if strings.Contains(meta.Data[0].ID, "pl.") { if !config.UseSongInfoForPlaylist { AlbumName = meta.Data[0].Attributes.Name } } err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, AlbumName) if err != nil { return err } err = addMeta(mp4.BoxType{'s', 'o', 'a', 'l'}, AlbumName) if err != nil { return err } err = addMeta(mp4.BoxType{'\251', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) if err != nil { return err } err = addMeta(mp4.BoxType{'s', 'o', 'a', 'r'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) if err != nil { return err } err = addMeta(mp4.BoxType{'\251', 'p', 'r', 'f'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) if err != nil { return err } err = addExtendedMeta("PERFORMER", meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) if err != nil { return err } err = addExtendedMeta("ITUNESALBUMID", meta.Data[0].ID) if err != nil { return err } err = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) if err != nil { return err } err = addMeta(mp4.BoxType{'s', 'o', 'c', 'o'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) if err != nil { return err } err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, meta.Data[0].Attributes.ReleaseDate) if err != nil { return err } 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 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.AlacSaveFolder, forbiddenNames.ReplaceAllString(singerFoldername, "_")) manifest1, err := getInfoFromAdam(meta.Data[0].Relationships.Tracks.Data[0].ID, token, storefront) if err != nil { fmt.Println("Failed to get manifest.\n", err) } if manifest1.Attributes.ExtendedAssetUrls.EnhancedHls == "" { fmt.Println("Unavailable in ALAC.") } var Quality string EnhancedHls_m3u8, err := checkM3u8(meta.Data[0].Relationships.Tracks.Data[0].ID, "album") if strings.HasPrefix(EnhancedHls_m3u8, "http") { manifest1.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 } if strings.Contains(config.AlbumFolderFormat, "Quality") { Quality, err = extractMediaQuality(manifest1.Attributes.ExtendedAssetUrls.EnhancedHls) if err != nil { fmt.Println("Failed to extract quality from manifest.\n", err) } } stringsToJoin := []string{} if meta.Data[0].Attributes.IsAppleDigitalMaster || meta.Data[0].Attributes.IsMasteredForItunes { if config.AppleMasterChoice != "" { stringsToJoin = append(stringsToJoin, config.AppleMasterChoice) } } if meta.Data[0].Attributes.ContentRating == "explicit" { if config.ExplicitChoice != "" { stringsToJoin = append(stringsToJoin, config.ExplicitChoice) } } if meta.Data[0].Attributes.ContentRating == "clean" { if config.CleanChoice != "" { stringsToJoin = append(stringsToJoin, config.CleanChoice) } } Tag_string := strings.Join(stringsToJoin, " ") var albumFolder string if strings.Contains(albumId, "pl.") { albumFolder = strings.NewReplacer( "{ArtistName}", "Apple Music", "{PlaylistName}", 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, "{RecordLabel}", meta.Data[0].Attributes.RecordLabel, "{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.")) { 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 } EnhancedHls_m3u8, err := checkM3u8(track.ID, "song") if strings.HasPrefix(EnhancedHls_m3u8, "http") { manifest.Attributes.ExtendedAssetUrls.EnhancedHls = EnhancedHls_m3u8 } 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, "{Tag}", Tag_string, "{Codec}", "ALAC", ).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.") oktrackNum += 1 continue } 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 track.Attributes.ContentRating == "explicit" { tags = append(tags, "rating=1") } else if track.Attributes.ContentRating == "clean" { tags = append(tags, "rating=2") } else { tags = append(tags, "rating=0") } if config.EmbedCover { if strings.Contains(albumId, "pl.") && config.DlAlbumcoverForPlaylist { err = writeCover(sanAlbumFolder, track.ID, track.Attributes.Artwork.URL) if err != nil { fmt.Println("Failed to write cover.") } tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, track.ID, config.CoverFormat)) } else { tags = append(tags, fmt.Sprintf("cover=%s/%s.%s", sanAlbumFolder, "cover", config.CoverFormat)) } } tagsString := strings.Join(tags, ":") cmd := exec.Command("MP4Box", "-itags", tagsString, trackPath) if err := cmd.Run(); err != nil { fmt.Printf("Embed failed: %v\n", err) 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 } } 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, ".txt") { txtpath = url fileName := filepath.Base(url) parts := strings.SplitN(fileName, "_", 3) storefront = parts[0] albumId = parts[1] } else { if strings.Contains(url, "/playlist/") { storefront, albumId = checkUrlPlaylist(url) txtpath = "" } else { storefront, albumId = checkUrl(url) txtpath = "" } } if albumId == "" { fmt.Printf("Invalid URL: %s\n", url) continue } err = rip(albumId, token, storefront, config.MediaUserToken) if err != nil { fmt.Println("Album failed.") fmt.Println(err) } } fmt.Printf("======= Completed %d/%d ###### %d errors!! =======\n", oktrackNum, trackTotalnum, trackTotalnum-oktrackNum) } func conventTTMLToLRC(ttml string) (string, error) { parsedTTML := etree.NewDocument() err := parsedTTML.ReadFromString(ttml) if err != nil { return "", err } var lrcLines []string for _, item := range parsedTTML.FindElement("tt").FindElement("body").ChildElements() { for _, lyric := range item.ChildElements() { var h, m, s, ms int if lyric.SelectAttr("begin") == nil { return "", errors.New("no synchronised lyrics") } if strings.Contains(lyric.SelectAttr("begin").Value, ":") { _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d:%d.%d", &h, &m, &s, &ms) if err != nil { _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d:%d.%d", &m, &s, &ms) h = 0 } } else { _, err = fmt.Sscanf(lyric.SelectAttr("begin").Value, "%d.%d", &s, &ms) h, m = 0, 0 } if err != nil { return "", err } var text string if lyric.SelectAttr("text") == nil { var textTmp []string for _, span := range lyric.Child { if _, ok := span.(*etree.CharData); ok { textTmp = append(textTmp, span.(*etree.CharData).Data) } else { textTmp = append(textTmp, span.(*etree.Element).Text()) } } text = strings.Join(textTmp, "") } else { text = lyric.SelectAttr("text").Value } m += h * 60 ms = ms / 10 lrcLines = append(lrcLines, fmt.Sprintf("[%02d:%02d.%02d]%s", m, s, ms, text)) } } return strings.Join(lrcLines, "\n"), nil } func checkM3u8(b string, f string) (string, error) { var EnhancedHls string if config.Check != "" { config.Check = strings.TrimSpace(config.Check) if strings.HasSuffix(config.Check, "txt") { txtpath = config.Check } if strings.HasPrefix(config.Check, "http") { req, err := http.NewRequest("GET", config.Check, nil) if err != nil { fmt.Println(err) } query := req.URL.Query() query.Set("songid", b) req.URL.RawQuery = query.Encode() do, err := http.DefaultClient.Do(req) if err != nil { fmt.Println(err) } defer do.Body.Close() Checkbody, err := ioutil.ReadAll(do.Body) if err != nil { fmt.Println(err) } if string(Checkbody) != "no_found" { EnhancedHls = string(Checkbody) fmt.Println("Found m3u8 from API") } else { if config.ForceApi { fmt.Println(" Not Found m3u8 from API, Skip") } fmt.Println(" Not Found m3u8 from API") } } } if config.GetM3u8FromDevice { adamID := b conn, err := net.Dial("tcp", config.GetM3u8Port) if err != nil { fmt.Println("Error connecting to device:", err) } defer conn.Close() if f == "song" { fmt.Println("Connected to device") } // Send the length of adamID and the adamID itself adamIDBuffer := []byte(adamID) lengthBuffer := []byte{byte(len(adamIDBuffer))} // Write length and adamID to the connection _, err = conn.Write(lengthBuffer) if err != nil { fmt.Println("Error writing length to device:", err) } _, err = conn.Write(adamIDBuffer) if err != nil { fmt.Println("Error writing adamID to device:", err) } // Read the response (URL) from the device response, err := bufio.NewReader(conn).ReadBytes('\n') if err != nil { fmt.Println("Error reading response from device:", err) } // Trim any newline characters from the response response = bytes.TrimSpace(response) if len(response) > 0 { if f == "song" { fmt.Println("Received URL:", string(response)) } EnhancedHls = string(response) } else { fmt.Println("Received an empty response") } } if txtpath != "" { file, err := os.Open(txtpath) if err != nil { fmt.Println("cant open txt:", err) } defer file.Close() scanner := bufio.NewScanner(file) for scanner.Scan() { line := scanner.Text() if strings.HasPrefix(line, b) { parts := strings.SplitN(line, ",", 2) if len(parts) == 2 { EnhancedHls = parts[1] fmt.Println("Found m3u8 from txt") } } } if err := scanner.Err(); err != nil { fmt.Println(err) } } return EnhancedHls, nil } func extractMediaQuality(b string) (string, error) { resp, err := http.Get(b) if err != nil { return "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", errors.New(resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return "", err } masterString := string(body) from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) if err != nil || listType != m3u8.MASTER { return "", errors.New("m3u8 not of master type") } master := from.(*m3u8.MasterPlaylist) sort.Slice(master.Variants, func(i, j int) bool { return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth }) var Quality string for _, variant := range master.Variants { if variant.Codecs == "alac" { split := strings.Split(variant.Audio, "-") length := len(split) length_int, err := strconv.Atoi(split[length-2]) if err != nil { return "", err } if length_int <= config.AlacMax { HZ, err := strconv.Atoi(split[length-2]) if err != nil { fmt.Println(err) } KHZ := float64(HZ) / 1000.0 Quality = fmt.Sprintf("%sB-%.1fkHz", split[length-1], KHZ) break } } } return Quality, nil } func extractMedia(b string) (string, []string, error) { masterUrl, err := url.Parse(b) if err != nil { return "", nil, err } resp, err := http.Get(b) if err != nil { return "", nil, err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", nil, errors.New(resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return "", nil, err } masterString := string(body) from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) if err != nil || listType != m3u8.MASTER { return "", nil, errors.New("m3u8 not of master type") } master := from.(*m3u8.MasterPlaylist) var streamUrl *url.URL sort.Slice(master.Variants, func(i, j int) bool { return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth }) for _, variant := range master.Variants { if 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 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"` }