├── .gitignore ├── README.md ├── agent.js ├── go.mod ├── go.sum ├── main.go └── main_atmos.go /.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore 3 | !agent.js 4 | !go.mod 5 | !go.sum 6 | !main_atmos.go 7 | !README.md 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Apple Music ALAC Downloader 2 | Original script by Sorrow. Modified by me to include some fixes and improvements. 3 | 4 | ## How to use 5 | 1. Create a virtual device on Android Studio with a image that doesn't have Google APIs. 6 | 2. Install this version of Apple Music: https://www.apkmirror.com/apk/apple/apple-music/apple-music-3-6-0-beta-release/apple-music-3-6-0-beta-4-android-apk-download/. You will also need SAI to install it: https://f-droid.org/pt_BR/packages/com.aefyr.sai.fdroid/. 7 | 3. Launch Apple Music and sign in to your account. Subscription required. 8 | 4. Port forward 10020 TCP: `adb forward tcp:10020 tcp:10020`. 9 | 5. Start frida server. 10 | 6. Start the frida agent: `frida -U -l agent.js -f com.apple.android.music`. 11 | 7. Start downloading some albums: `go run main.go https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511`. 12 | 8. For dolby atmos: `go run main_atmos.go "https://music.apple.com/hk/album/%E5%91%A8%E6%9D%B0%E5%80%AB%E5%9C%B0%E8%A1%A8%E6%9C%80%E5%BC%B7%E4%B8%96%E7%95%8C%E5%B7%A1%E8%BF%B4%E6%BC%94%E5%94%B1%E6%9C%83/1721464851"`. 13 | -------------------------------------------------------------------------------- /agent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | setTimeout(() => { 3 | const fairplayCert = "MIIEzjCCA7agAwIBAgIIAXAVjHFZDjgwDQYJKoZIhvcNAQEFBQAwfzELMAkGA1UEBhMCVVMxEzARBgNVBAoMCkFwcGxlIEluYy4xJjAkBgNVBAsMHUFwcGxlIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MTMwMQYDVQQDDCpBcHBsZSBLZXkgU2VydmljZXMgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkwHhcNMTIwNzI1MTgwMjU4WhcNMTQwNzI2MTgwMjU4WjAwMQswCQYDVQQGEwJVUzESMBAGA1UECgwJQXBwbGUgSW5jMQ0wCwYDVQQDDARGUFMxMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCqZ9IbMt0J0dTKQN4cUlfeQRY9bcnbnP95HFv9A16Yayh4xQzRLAQqVSmisZtBK2/nawZcDmcs+XapBojRb+jDM4Dzk6/Ygdqo8LoA+BE1zipVyalGLj8Y86hTC9QHX8i05oWNCDIlmabjjWvFBoEOk+ezOAPg8c0SET38x5u+TwIDAQABo4ICHzCCAhswHQYDVR0OBBYEFPP6sfTWpOQ5Sguf5W3Y0oibbEc3MAwGA1UdEwEB/wQCMAAwHwYDVR0jBBgwFoAUY+RHVMuFcVlGLIOszEQxZGcDLL4wgeIGA1UdIASB2jCB1zCB1AYJKoZIhvdjZAUBMIHGMIHDBggrBgEFBQcCAjCBtgyBs1JlbGlhbmNlIG9uIHRoaXMgY2VydGlmaWNhdGUgYnkgYW55IHBhcnR5IGFzc3VtZXMgYWNjZXB0YW5jZSBvZiB0aGUgdGhlbiBhcHBsaWNhYmxlIHN0YW5kYXJkIHRlcm1zIGFuZCBjb25kaXRpb25zIG9mIHVzZSwgY2VydGlmaWNhdGUgcG9saWN5IGFuZCBjZXJ0aWZpY2F0aW9uIHByYWN0aWNlIHN0YXRlbWVudHMuMDUGA1UdHwQuMCwwKqAooCaGJGh0dHA6Ly9jcmwuYXBwbGUuY29tL2tleXNlcnZpY2VzLmNybDAOBgNVHQ8BAf8EBAMCBSAwFAYLKoZIhvdjZAYNAQUBAf8EAgUAMBsGCyqGSIb3Y2QGDQEGAQH/BAkBAAAAAQAAAAEwKQYLKoZIhvdjZAYNAQMBAf8EFwF+bjsY57ASVFmeehD2bdu6HLGBxeC2MEEGCyqGSIb3Y2QGDQEEAQH/BC8BHrKviHJf/Se/ibc7T0/55Bt1GePzaYBVfgF3ZiNuV93z8P3qsawAqAXzzh9o5DANBgkqhkiG9w0BAQUFAAOCAQEAVGyCtuLYcYb/aPijBCtaemxuV0IokXJn3EgmwYHZynaR6HZmeGRUp9p3f8EXu6XPSekKCCQi+a86hXX9RfnGEjRdvtP+jts5MDSKuUIoaqce8cLX2dpUOZXdf3lR0IQM0kXHb5boNGBsmbTLVifqeMsexfZryGw2hE/4WDOJdGQm1gMJZU4jP1b/HSLNIUhHWAaMeWtcJTPRBucR4urAtvvtOWD88mriZNHG+veYw55b+qA36PSqDPMbku9xTY7fsMa6mxIRmwULQgi8nOk1wNhw3ZO0qUKtaCO3gSqWdloecxpxUQSZCSW7tWPkpXXwDZqegUkij9xMFS1pr37RIg=="; 4 | 5 | function newStdStringFromBuffer(content) { 6 | const size = content.byteLength; 7 | const cap = 2 ** Math.ceil(Math.log2(size + 1)); 8 | const buffer = Memory.alloc(cap); 9 | Memory.copy(buffer, content.unwrap(), size); 10 | 11 | const addr = Memory.alloc(Process.pointerSize * 3); 12 | addr.writeULong(cap | 0x1); 13 | addr.add(Process.pointerSize).writeULong(size); 14 | addr.add(Process.pointerSize * 2).writePointer(buffer); 15 | 16 | return { buffer: buffer, str: addr }; 17 | } 18 | 19 | function newStdString(content) { 20 | const size = content.length; 21 | const cap = 2 ** Math.ceil(Math.log2(size + 1)); 22 | const buffer = Memory.alloc(cap); 23 | buffer.writeUtf8String(content); 24 | 25 | const addr = Memory.alloc(Process.pointerSize * 3); 26 | addr.writeULong(cap | 0x1); 27 | addr.add(Process.pointerSize).writeULong(size); 28 | addr.add(Process.pointerSize * 2).writePointer(buffer); 29 | 30 | return { buffer: buffer, str: addr }; 31 | } 32 | 33 | 34 | const androidappmusic = Process.getModuleByName("libandroidappmusic.so"); 35 | 36 | const sessionCtrlPtr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl8instanceEv"); 37 | const sessionCtrlInstanceFunc = new NativeFunction(sessionCtrlPtr, "pointer", []); 38 | const sessionCtrlInstance = sessionCtrlInstanceFunc(); 39 | 40 | const getPersistentKeyAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl16getPersistentKeyERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEES8_S8_S8_S8_S8_S8_"); 41 | const getPersistentKey = new NativeFunction(getPersistentKeyAddr, "void", Array(9).fill("pointer")); 42 | 43 | const decryptContextAddr = androidappmusic.getExportByName("_ZN21SVFootHillSessionCtrl14decryptContextERKNSt6__ndk112basic_stringIcNS0_11char_traitsIcEENS0_9allocatorIcEEEERKN11SVDecryptor15SVDecryptorTypeERKb"); 44 | const decryptContext = new NativeFunction(decryptContextAddr, "void", Array(3).fill("pointer")); 45 | 46 | const NfcRKVnxuKZy04KWbdFu71Ou = androidappmusic.getExportByName("NfcRKVnxuKZy04KWbdFu71Ou"); 47 | const decryptSample = new NativeFunction(NfcRKVnxuKZy04KWbdFu71Ou, 'ulong', ['pointer', 'uint', 'pointer', 'pointer', 'size_t']); 48 | 49 | const kdContextMap = new Map(); 50 | 51 | function getkdContext(adam, uri) { 52 | const uriStr = String.fromCharCode(...new Uint8Array(uri)) 53 | if (kdContextMap.has(uriStr)) { 54 | return kdContextMap.get(uriStr); 55 | } 56 | 57 | const defaultId = newStdStringFromBuffer(adam); 58 | const keyUri = newStdStringFromBuffer(uri); 59 | const keyFormat = newStdString("com.apple.streamingkeydelivery"); 60 | const keyFormatVer = newStdString("1"); 61 | const serverUri = newStdString("https://play.itunes.apple.com/WebObjects/MZPlay.woa/music/fps"); 62 | const protocolType = newStdString("simplified"); 63 | const fpsCert = newStdString(fairplayCert); 64 | const persistentKey = Memory.alloc(Process.pointerSize * 2); 65 | getPersistentKey(persistentKey, sessionCtrlInstance, defaultId.str, keyUri.str, keyFormat.str, keyFormatVer.str, serverUri.str, protocolType.str, fpsCert.str); 66 | 67 | const ptr = persistentKey.readPointer(); 68 | if (ptr.isNull()) return null; 69 | 70 | const svfootHillPKey = Memory.alloc(Process.pointerSize * 2); 71 | decryptContext(svfootHillPKey, sessionCtrlInstance, ptr); 72 | 73 | const ptr2 = svfootHillPKey.readPointer(); 74 | if (ptr2.isNull()) return null; 75 | 76 | const ap = ptr2.add(0x18).readPointer(); 77 | if (!ap.isNull()) kdContextMap.set(uriStr, ap); 78 | return ap; 79 | } 80 | 81 | async function handleConnection(s) { 82 | // console.log("new connection!"); 83 | while (true) { 84 | const adamSize = (await s.input.readAll(1)).unwrap().readU8(); 85 | if (adamSize === 0) 86 | break; 87 | const adam = await s.input.readAll(adamSize); 88 | const uriSize = (await s.input.readAll(1)).unwrap().readU8(); 89 | const uri = await s.input.readAll(uriSize); 90 | const kdContext = getkdContext(adam, uri); 91 | // console.log(adam, uri, kdContext) 92 | while (true) { 93 | const size = (await s.input.readAll(4)).unwrap().readU32(); 94 | if (size === 0) 95 | break; 96 | const sample = await s.input.readAll(size); 97 | decryptSample(kdContext.readPointer(), 5, sample.unwrap(), sample.unwrap(), sample.byteLength); 98 | await s.output.writeAll(sample); 99 | } 100 | } 101 | await s.close(); 102 | } 103 | 104 | Socket.listen({ 105 | family: "ipv4", 106 | port: 10020, 107 | }).then(async function (listener) { 108 | while (true) { 109 | handleConnection(await listener.accept()); 110 | } 111 | }).catch(console.log); 112 | }, 4000); -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module main 2 | 3 | go 1.17 4 | 5 | require ( 6 | github.com/abema/go-mp4 v0.7.2 7 | github.com/grafov/m3u8 v0.11.1 8 | ) 9 | 10 | require github.com/google/uuid v1.1.2 // indirect 11 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/abema/go-mp4 v0.7.2 h1:ugTC8gfEmjyaDKpXs3vi2QzgJbDu9B8m6UMMIpbYbGg= 2 | github.com/abema/go-mp4 v0.7.2/go.mod h1:vPl9t5ZK7K0x68jh12/+ECWBCXoWuIDtNgPtU2f04ws= 3 | github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y= 7 | github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 8 | github.com/grafov/m3u8 v0.11.1 h1:igZ7EBIB2IAsPPazKwRKdbhxcoBKO3lO1UY57PZDeNA= 9 | github.com/grafov/m3u8 v0.11.1/go.mod h1:nqzOkfBiZJENr52zTVd/Dcl03yzphIMbJqkXGu+u080= 10 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 11 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 12 | github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= 13 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 14 | github.com/orcaman/writerseeker v0.0.0-20200621085525-1d3f536ff85e/go.mod h1:nBdnFKj15wFbf94Rwfq4m30eAcyY9V/IyKAGQFtqkW0= 15 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 16 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 17 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 18 | github.com/sunfish-shogi/bufseekio v0.0.0-20210207115823-a4185644b365/go.mod h1:dEzdXgvImkQ3WLI+0KQpmEx8T/C/ma9KeS3AfmU899I= 19 | golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 20 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 21 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 23 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 24 | gopkg.in/src-d/go-billy.v4 v4.3.2/go.mod h1:nDjArDMp+XMs1aFAESLRjfGSgfvoYN0hDfzEk0GjC98= 25 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 26 | gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 27 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "regexp" 18 | "sort" 19 | "strings" 20 | "time" 21 | 22 | "github.com/abema/go-mp4" 23 | "github.com/grafov/m3u8" 24 | ) 25 | 26 | const ( 27 | defaultId = "0" 28 | prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" 29 | ) 30 | 31 | var ( 32 | forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) 33 | ) 34 | 35 | type SampleInfo struct { 36 | data []byte 37 | duration uint32 38 | descIndex uint32 39 | } 40 | 41 | type SongInfo struct { 42 | r io.ReadSeeker 43 | alacParam *Alac 44 | samples []SampleInfo 45 | } 46 | 47 | func (s *SongInfo) Duration() (ret uint64) { 48 | for i := range s.samples { 49 | ret += uint64(s.samples[i].duration) 50 | } 51 | return 52 | } 53 | 54 | func (*Alac) GetType() mp4.BoxType { 55 | return BoxTypeAlac() 56 | } 57 | 58 | func fileExists(path string) (bool, error) { 59 | f, err := os.Stat(path) 60 | if err == nil { 61 | return !f.IsDir(), nil 62 | } else if os.IsNotExist(err) { 63 | return false, nil 64 | } 65 | return false, err 66 | } 67 | 68 | func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error { 69 | index := trackNum - 1 70 | { // ftyp 71 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()}) 72 | if err != nil { 73 | return err 74 | } 75 | _, err = mp4.Marshal(w, &mp4.Ftyp{ 76 | MajorBrand: [4]byte{'M', '4', 'A', ' '}, 77 | MinorVersion: 0, 78 | CompatibleBrands: []mp4.CompatibleBrandElem{ 79 | {CompatibleBrand: [4]byte{'M', '4', 'A', ' '}}, 80 | {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, 81 | {CompatibleBrand: mp4.BrandISOM()}, 82 | {CompatibleBrand: [4]byte{0, 0, 0, 0}}, 83 | }, 84 | }, box.Context) 85 | if err != nil { 86 | return err 87 | } 88 | _, err = w.EndBox() 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | const chunkSize uint32 = 5 95 | duration := info.Duration() 96 | numSamples := uint32(len(info.samples)) 97 | var stco *mp4.BoxInfo 98 | 99 | { // moov 100 | _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()}) 101 | if err != nil { 102 | return err 103 | } 104 | box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()}) 105 | if err != nil { 106 | return err 107 | } 108 | moovOri := box[0] 109 | 110 | { // mvhd 111 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()}) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()}) 117 | if err != nil { 118 | return err 119 | } 120 | mvhd := oriBox[0].Payload.(*mp4.Mvhd) 121 | if mvhd.Version == 0 { 122 | mvhd.DurationV0 = uint32(duration) 123 | } else { 124 | mvhd.DurationV1 = duration 125 | } 126 | 127 | _, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | _, err = w.EndBox() 133 | if err != nil { 134 | return err 135 | } 136 | } 137 | 138 | { // trak 139 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()}) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()}) 145 | if err != nil { 146 | return err 147 | } 148 | trakOri := box[0] 149 | 150 | { // tkhd 151 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()}) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()}) 157 | if err != nil { 158 | return err 159 | } 160 | tkhd := oriBox[0].Payload.(*mp4.Tkhd) 161 | if tkhd.Version == 0 { 162 | tkhd.DurationV0 = uint32(duration) 163 | } else { 164 | tkhd.DurationV1 = duration 165 | } 166 | tkhd.SetFlags(0x7) 167 | 168 | _, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | _, err = w.EndBox() 174 | if err != nil { 175 | return err 176 | } 177 | } 178 | 179 | { // mdia 180 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()}) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()}) 186 | if err != nil { 187 | return err 188 | } 189 | mdiaOri := box[0] 190 | 191 | { // mdhd 192 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()}) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()}) 198 | if err != nil { 199 | return err 200 | } 201 | mdhd := oriBox[0].Payload.(*mp4.Mdhd) 202 | if mdhd.Version == 0 { 203 | mdhd.DurationV0 = uint32(duration) 204 | } else { 205 | mdhd.DurationV1 = duration 206 | } 207 | 208 | _, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | _, err = w.EndBox() 214 | if err != nil { 215 | return err 216 | } 217 | } 218 | 219 | { // hdlr 220 | oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()}) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | err = w.CopyBox(info.r, oriBox[0]) 226 | if err != nil { 227 | return err 228 | } 229 | } 230 | 231 | { // minf 232 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()}) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()}) 238 | if err != nil { 239 | return err 240 | } 241 | minfOri := box[0] 242 | 243 | { // smhd, dinf 244 | boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{ 245 | {mp4.BoxTypeSmhd()}, 246 | {mp4.BoxTypeDinf()}, 247 | }) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | for _, b := range boxes { 253 | err = w.CopyBox(info.r, b) 254 | if err != nil { 255 | return err 256 | } 257 | } 258 | } 259 | 260 | { // stbl 261 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()}) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | { // stsd 267 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()}) 268 | if err != nil { 269 | return err 270 | } 271 | _, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context) 272 | if err != nil { 273 | return err 274 | } 275 | 276 | { // alac 277 | _, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | _, err = w.Write([]byte{ 283 | 0, 0, 0, 0, 0, 0, 0, 1, 284 | 0, 0, 0, 0, 0, 0, 0, 0}) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels)) 290 | if err != nil { 291 | return err 292 | } 293 | 294 | err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth)) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | _, err = w.Write([]byte{0, 0}) 300 | if err != nil { 301 | return err 302 | } 303 | 304 | err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | _, err = w.Write([]byte{0, 0}) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | _, err = mp4.Marshal(w, info.alacParam, box.Context) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | _, err = w.EndBox() 325 | if err != nil { 326 | return err 327 | } 328 | 329 | _, err = w.EndBox() 330 | if err != nil { 331 | return err 332 | } 333 | } 334 | 335 | _, err = w.EndBox() 336 | if err != nil { 337 | return err 338 | } 339 | } 340 | 341 | { // stts 342 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()}) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | var stts mp4.Stts 348 | for _, sample := range info.samples { 349 | if len(stts.Entries) != 0 { 350 | last := &stts.Entries[len(stts.Entries)-1] 351 | if last.SampleDelta == sample.duration { 352 | last.SampleCount++ 353 | continue 354 | } 355 | } 356 | stts.Entries = append(stts.Entries, mp4.SttsEntry{ 357 | SampleCount: 1, 358 | SampleDelta: sample.duration, 359 | }) 360 | } 361 | stts.EntryCount = uint32(len(stts.Entries)) 362 | 363 | _, err = mp4.Marshal(w, &stts, box.Context) 364 | if err != nil { 365 | return err 366 | } 367 | 368 | _, err = w.EndBox() 369 | if err != nil { 370 | return err 371 | } 372 | } 373 | 374 | { // stsc 375 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()}) 376 | if err != nil { 377 | return err 378 | } 379 | 380 | if numSamples%chunkSize == 0 { 381 | _, err = mp4.Marshal(w, &mp4.Stsc{ 382 | EntryCount: 1, 383 | Entries: []mp4.StscEntry{ 384 | { 385 | FirstChunk: 1, 386 | SamplesPerChunk: chunkSize, 387 | SampleDescriptionIndex: 1, 388 | }, 389 | }, 390 | }, box.Context) 391 | } else { 392 | _, err = mp4.Marshal(w, &mp4.Stsc{ 393 | EntryCount: 2, 394 | Entries: []mp4.StscEntry{ 395 | { 396 | FirstChunk: 1, 397 | SamplesPerChunk: chunkSize, 398 | SampleDescriptionIndex: 1, 399 | }, { 400 | FirstChunk: numSamples/chunkSize + 1, 401 | SamplesPerChunk: numSamples % chunkSize, 402 | SampleDescriptionIndex: 1, 403 | }, 404 | }, 405 | }, box.Context) 406 | } 407 | 408 | _, err = w.EndBox() 409 | if err != nil { 410 | return err 411 | } 412 | } 413 | 414 | { // stsz 415 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()}) 416 | if err != nil { 417 | return err 418 | } 419 | 420 | stsz := mp4.Stsz{SampleCount: numSamples} 421 | for _, sample := range info.samples { 422 | stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data))) 423 | } 424 | 425 | _, err = mp4.Marshal(w, &stsz, box.Context) 426 | if err != nil { 427 | return err 428 | } 429 | 430 | _, err = w.EndBox() 431 | if err != nil { 432 | return err 433 | } 434 | } 435 | 436 | { // stco 437 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()}) 438 | if err != nil { 439 | return err 440 | } 441 | 442 | l := (numSamples + chunkSize - 1) / chunkSize 443 | _, err = mp4.Marshal(w, &mp4.Stco{ 444 | EntryCount: l, 445 | ChunkOffset: make([]uint32, l), 446 | }, box.Context) 447 | 448 | stco, err = w.EndBox() 449 | if err != nil { 450 | return err 451 | } 452 | } 453 | 454 | _, err = w.EndBox() 455 | if err != nil { 456 | return err 457 | } 458 | } 459 | 460 | _, err = w.EndBox() 461 | if err != nil { 462 | return err 463 | } 464 | } 465 | 466 | _, err = w.EndBox() 467 | if err != nil { 468 | return err 469 | } 470 | } 471 | 472 | _, err = w.EndBox() 473 | if err != nil { 474 | return err 475 | } 476 | } 477 | 478 | { // udta 479 | ctx := mp4.Context{UnderUdta: true} 480 | _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx}) 481 | if err != nil { 482 | return err 483 | } 484 | 485 | { // meta 486 | ctx.UnderIlstMeta = true 487 | 488 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx}) 489 | if err != nil { 490 | return err 491 | } 492 | 493 | _, err = mp4.Marshal(w, &mp4.Meta{}, ctx) 494 | if err != nil { 495 | return err 496 | } 497 | 498 | { // hdlr 499 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx}) 500 | if err != nil { 501 | return err 502 | } 503 | 504 | _, err = mp4.Marshal(w, &mp4.Hdlr{ 505 | HandlerType: [4]byte{'m', 'd', 'i', 'r'}, 506 | Reserved: [3]uint32{0x6170706c, 0, 0}, 507 | }, ctx) 508 | if err != nil { 509 | return err 510 | } 511 | 512 | _, err = w.EndBox() 513 | if err != nil { 514 | return err 515 | } 516 | } 517 | 518 | { // ilst 519 | ctx.UnderIlst = true 520 | 521 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx}) 522 | if err != nil { 523 | return err 524 | } 525 | 526 | marshalData := func(val interface{}) error { 527 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) 528 | if err != nil { 529 | return err 530 | } 531 | 532 | var boxData mp4.Data 533 | switch v := val.(type) { 534 | case string: 535 | boxData.DataType = mp4.DataTypeStringUTF8 536 | boxData.Data = []byte(v) 537 | case uint8: 538 | boxData.DataType = mp4.DataTypeSignedIntBigEndian 539 | boxData.Data = []byte{v} 540 | case uint32: 541 | boxData.DataType = mp4.DataTypeSignedIntBigEndian 542 | boxData.Data = make([]byte, 4) 543 | binary.BigEndian.PutUint32(boxData.Data, v) 544 | case []byte: 545 | boxData.DataType = mp4.DataTypeBinary 546 | boxData.Data = v 547 | default: 548 | panic("unsupported value") 549 | } 550 | 551 | _, err = mp4.Marshal(w, &boxData, ctx) 552 | if err != nil { 553 | return err 554 | } 555 | 556 | _, err = w.EndBox() 557 | return err 558 | } 559 | 560 | addMeta := func(tag mp4.BoxType, val interface{}) error { 561 | _, err = w.StartBox(&mp4.BoxInfo{Type: tag}) 562 | if err != nil { 563 | return err 564 | } 565 | 566 | err = marshalData(val) 567 | if err != nil { 568 | return err 569 | } 570 | 571 | _, err = w.EndBox() 572 | return err 573 | } 574 | 575 | addExtendedMeta := func(name string, val interface{}) error { 576 | ctx.UnderIlstFreeMeta = true 577 | 578 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx}) 579 | if err != nil { 580 | return err 581 | } 582 | 583 | { 584 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx}) 585 | if err != nil { 586 | return err 587 | } 588 | 589 | _, err = w.Write([]byte{0, 0, 0, 0}) 590 | if err != nil { 591 | return err 592 | } 593 | 594 | _, err = io.WriteString(w, "com.apple.iTunes") 595 | if err != nil { 596 | return err 597 | } 598 | 599 | _, err = w.EndBox() 600 | if err != nil { 601 | return err 602 | } 603 | } 604 | 605 | { 606 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx}) 607 | if err != nil { 608 | return err 609 | } 610 | 611 | _, err = w.Write([]byte{0, 0, 0, 0}) 612 | if err != nil { 613 | return err 614 | } 615 | 616 | _, err = io.WriteString(w, name) 617 | if err != nil { 618 | return err 619 | } 620 | 621 | _, err = w.EndBox() 622 | if err != nil { 623 | return err 624 | } 625 | } 626 | 627 | err = marshalData(val) 628 | if err != nil { 629 | return err 630 | } 631 | 632 | ctx.UnderIlstFreeMeta = false 633 | 634 | _, err = w.EndBox() 635 | return err 636 | } 637 | 638 | err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) 639 | if err != nil { 640 | return err 641 | } 642 | 643 | err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, meta.Data[0].Attributes.Name) 644 | if err != nil { 645 | return err 646 | } 647 | 648 | err = addMeta(mp4.BoxType{'\251', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) 649 | if err != nil { 650 | return err 651 | } 652 | 653 | err = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) 654 | if err != nil { 655 | return err 656 | } 657 | 658 | err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, strings.Split(meta.Data[0].Attributes.ReleaseDate, "-")[0]) 659 | if err != nil { 660 | return err 661 | } 662 | 663 | // cnID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].ID, 10, 32) 664 | // if err != nil { 665 | // return err 666 | // } 667 | 668 | // err = addMeta(mp4.BoxType{'c', 'n', 'I', 'D'}, uint32(cnID)) 669 | // if err != nil { 670 | // return err 671 | // } 672 | 673 | err = addExtendedMeta("ISRC", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc) 674 | if err != nil { 675 | return err 676 | } 677 | 678 | if len(meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames) > 0 { 679 | err = addMeta(mp4.BoxType{'\251', 'g', 'e', 'n'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]) 680 | if err != nil { 681 | return err 682 | } 683 | } 684 | 685 | if len(meta.Data) > 0 { 686 | album := meta.Data[0] 687 | 688 | err = addMeta(mp4.BoxType{'a', 'A', 'R', 'T'}, album.Attributes.ArtistName) 689 | if err != nil { 690 | return err 691 | } 692 | 693 | err = addMeta(mp4.BoxType{'c', 'p', 'r', 't'}, album.Attributes.Copyright) 694 | if err != nil { 695 | return err 696 | } 697 | 698 | var isCpil uint8 699 | if album.Attributes.IsCompilation { 700 | isCpil = 1 701 | } 702 | err = addMeta(mp4.BoxType{'c', 'p', 'i', 'l'}, isCpil) 703 | if err != nil { 704 | return err 705 | } 706 | 707 | err = addExtendedMeta("LABEL", album.Attributes.RecordLabel) 708 | if err != nil { 709 | return err 710 | } 711 | 712 | err = addExtendedMeta("UPC", album.Attributes.Upc) 713 | if err != nil { 714 | return err 715 | } 716 | 717 | // plID, err := strconv.ParseUint(album.ID, 10, 32) 718 | // if err != nil { 719 | // return err 720 | // } 721 | 722 | // err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID)) 723 | // if err != nil { 724 | // return err 725 | // } 726 | } 727 | 728 | // if len(meta.Data[0].Relationships.Artists.Data) > 0 { 729 | // atID, err := strconv.ParseUint(meta.Data[0].Relationships.Artists.Data[index].ID, 10, 32) 730 | // if err != nil { 731 | // return err 732 | // } 733 | 734 | // err = addMeta(mp4.BoxType{'a', 't', 'I', 'D'}, uint32(atID)) 735 | // if err != nil { 736 | // return err 737 | // } 738 | // } 739 | 740 | trkn := make([]byte, 8) 741 | binary.BigEndian.PutUint32(trkn, uint32(trackNum)) 742 | binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) 743 | err = addMeta(mp4.BoxType{'t', 'r', 'k', 'n'}, trkn) 744 | if err != nil { 745 | return err 746 | } 747 | 748 | // disk := make([]byte, 8) 749 | // binary.BigEndian.PutUint32(disk, uint32(meta.Attributes.DiscNumber)) 750 | // err = addMeta(mp4.BoxType{'d', 'i', 's', 'k'}, disk) 751 | // if err != nil { 752 | // return err 753 | // } 754 | 755 | ctx.UnderIlst = false 756 | 757 | _, err = w.EndBox() 758 | if err != nil { 759 | return err 760 | } 761 | } 762 | 763 | ctx.UnderIlstMeta = false 764 | _, err = w.EndBox() 765 | if err != nil { 766 | return err 767 | } 768 | } 769 | 770 | ctx.UnderUdta = false 771 | _, err = w.EndBox() 772 | if err != nil { 773 | return err 774 | } 775 | } 776 | 777 | _, err = w.EndBox() 778 | if err != nil { 779 | return err 780 | } 781 | } 782 | 783 | { 784 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdat()}) 785 | if err != nil { 786 | return err 787 | } 788 | 789 | _, err = mp4.Marshal(w, &mp4.Mdat{Data: data}, box.Context) 790 | if err != nil { 791 | return err 792 | } 793 | 794 | mdat, err := w.EndBox() 795 | 796 | var realStco mp4.Stco 797 | 798 | offset := mdat.Offset + mdat.HeaderSize 799 | for i := uint32(0); i < numSamples; i++ { 800 | if i%chunkSize == 0 { 801 | realStco.EntryCount++ 802 | realStco.ChunkOffset = append(realStco.ChunkOffset, uint32(offset)) 803 | } 804 | offset += uint64(len(info.samples[i].data)) 805 | } 806 | 807 | _, err = stco.SeekToPayload(w) 808 | if err != nil { 809 | return err 810 | } 811 | _, err = mp4.Marshal(w, &realStco, box.Context) 812 | if err != nil { 813 | return err 814 | } 815 | } 816 | 817 | return nil 818 | } 819 | 820 | func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filename string, trackNum, trackTotal int) error { 821 | //fmt.Printf("%d-bit / %d Hz\n", info.bitDepth, info.bitRate) 822 | conn, err := net.Dial("tcp", "127.0.0.1:10020") 823 | if err != nil { 824 | return err 825 | } 826 | defer conn.Close() 827 | var decrypted []byte 828 | var lastIndex uint32 = math.MaxUint8 829 | 830 | fmt.Println("Decrypt start.") 831 | for _, sp := range info.samples { 832 | if lastIndex != sp.descIndex { 833 | if len(decrypted) != 0 { 834 | _, err := conn.Write([]byte{0, 0, 0, 0}) 835 | if err != nil { 836 | return err 837 | } 838 | } 839 | keyUri := keys[sp.descIndex] 840 | id := manifest.Data[0].Relationships.Tracks.Data[trackNum-1].ID 841 | if keyUri == prefetchKey { 842 | id = defaultId 843 | } 844 | 845 | _, err := conn.Write([]byte{byte(len(id))}) 846 | if err != nil { 847 | return err 848 | } 849 | _, err = io.WriteString(conn, id) 850 | if err != nil { 851 | return err 852 | } 853 | 854 | _, err = conn.Write([]byte{byte(len(keyUri))}) 855 | if err != nil { 856 | return err 857 | } 858 | _, err = io.WriteString(conn, keyUri) 859 | if err != nil { 860 | return err 861 | } 862 | } 863 | lastIndex = sp.descIndex 864 | 865 | err := binary.Write(conn, binary.LittleEndian, uint32(len(sp.data))) 866 | if err != nil { 867 | return err 868 | } 869 | 870 | _, err = conn.Write(sp.data) 871 | if err != nil { 872 | return err 873 | } 874 | 875 | de := make([]byte, len(sp.data)) 876 | _, err = io.ReadFull(conn, de) 877 | if err != nil { 878 | return err 879 | } 880 | 881 | decrypted = append(decrypted, de...) 882 | } 883 | _, _ = conn.Write([]byte{0, 0, 0, 0, 0}) 884 | 885 | fmt.Println("Decrypt finished.") 886 | 887 | create, err := os.Create(filename) 888 | if err != nil { 889 | return err 890 | } 891 | defer create.Close() 892 | 893 | return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal) 894 | } 895 | 896 | func checkUrl(url string) (string, string) { 897 | pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) 898 | matches := pat.FindAllStringSubmatch(url, -1) 899 | if matches == nil { 900 | return "", "" 901 | } else { 902 | return matches[0][1], matches[0][2] 903 | } 904 | } 905 | 906 | func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { 907 | req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/albums/%s", storefront, albumId), nil) 908 | if err != nil { 909 | return nil, err 910 | } 911 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 912 | 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") 913 | req.Header.Set("Origin", "https://music.apple.com") 914 | query := url.Values{} 915 | query.Set("omit[resource]", "autos") 916 | query.Set("include", "tracks,artists,record-labels") 917 | query.Set("include[songs]", "artists") 918 | query.Set("fields[artists]", "name") 919 | query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url") 920 | query.Set("fields[record-labels]", "name") 921 | // query.Set("l", "en-gb") 922 | req.URL.RawQuery = query.Encode() 923 | do, err := http.DefaultClient.Do(req) 924 | if err != nil { 925 | return nil, err 926 | } 927 | defer do.Body.Close() 928 | if do.StatusCode != http.StatusOK { 929 | return nil, errors.New(do.Status) 930 | } 931 | obj := new(AutoGenerated) 932 | err = json.NewDecoder(do.Body).Decode(&obj) 933 | if err != nil { 934 | return nil, err 935 | } 936 | return obj, nil 937 | } 938 | 939 | func writeCover(sanAlbumFolder, url string) error { 940 | covPath := filepath.Join(sanAlbumFolder, "cover.jpg") 941 | exists, err := fileExists(covPath) 942 | if err != nil { 943 | fmt.Println("Failed to check if cover exists.") 944 | return err 945 | } 946 | if exists { 947 | return nil 948 | } 949 | url = strings.Replace(url, "{w}x{h}", "1200x12000", 1) 950 | req, err := http.NewRequest("GET", url, nil) 951 | if err != nil { 952 | return err 953 | } 954 | 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") 955 | do, err := http.DefaultClient.Do(req) 956 | if err != nil { 957 | return err 958 | } 959 | defer do.Body.Close() 960 | if do.StatusCode != http.StatusOK { 961 | errors.New(do.Status) 962 | } 963 | f, err := os.Create(covPath) 964 | if err != nil { 965 | return err 966 | } 967 | defer f.Close() 968 | _, err = io.Copy(f, do.Body) 969 | if err != nil { 970 | return err 971 | } 972 | return nil 973 | } 974 | 975 | func rip(albumId string, token string, storefront string) error { 976 | meta, err := getMeta(albumId, token, storefront) 977 | if err != nil { 978 | fmt.Println("Failed to get album metadata.\n") 979 | return err 980 | } 981 | albumFolder := fmt.Sprintf("%s - %s", meta.Data[0].Attributes.ArtistName, meta.Data[0].Attributes.Name) 982 | sanAlbumFolder := filepath.Join("AM-DL downloads", forbiddenNames.ReplaceAllString(albumFolder, "_")) 983 | os.MkdirAll(sanAlbumFolder, os.ModePerm) 984 | fmt.Println(albumFolder) 985 | err = writeCover(sanAlbumFolder, meta.Data[0].Attributes.Artwork.URL) 986 | if err != nil { 987 | fmt.Println("Failed to write cover.") 988 | } 989 | trackTotal := len(meta.Data[0].Relationships.Tracks.Data) 990 | for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { 991 | trackNum++ 992 | fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) 993 | manifest, err := getInfoFromAdam(track.ID, token, storefront) 994 | if err != nil { 995 | fmt.Println("Failed to get manifest.\n", err) 996 | continue 997 | } 998 | if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { 999 | fmt.Println("Unavailable in ALAC.") 1000 | continue 1001 | } 1002 | filename := fmt.Sprintf("%02d. %s.m4a", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) 1003 | trackPath := filepath.Join(sanAlbumFolder, filename) 1004 | exists, err := fileExists(trackPath) 1005 | if err != nil { 1006 | fmt.Println("Failed to check if track exists.") 1007 | } 1008 | if exists { 1009 | fmt.Println("Track already exists locally.") 1010 | continue 1011 | } 1012 | trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) 1013 | if err != nil { 1014 | fmt.Println("Failed to extract info from manifest.\n", err) 1015 | continue 1016 | } 1017 | info, err := extractSong(trackUrl) 1018 | if err != nil { 1019 | fmt.Println("Failed to extract track.", err) 1020 | continue 1021 | } 1022 | samplesOk := true 1023 | for samplesOk { 1024 | for _, i := range info.samples { 1025 | if int(i.descIndex) >= len(keys) { 1026 | fmt.Println("Decryption size mismatch.") 1027 | samplesOk = false 1028 | } 1029 | } 1030 | break 1031 | } 1032 | if !samplesOk { 1033 | continue 1034 | } 1035 | err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) 1036 | if err != nil { 1037 | fmt.Println("Failed to decrypt track.\n", err) 1038 | continue 1039 | } 1040 | } 1041 | return err 1042 | } 1043 | 1044 | func main() { 1045 | token, err := getToken() 1046 | if err != nil { 1047 | fmt.Println("Failed to get token.") 1048 | return 1049 | } 1050 | albumTotal := len(os.Args[1:]) 1051 | for albumNum, url := range os.Args[1:] { 1052 | fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) 1053 | storefront, albumId := checkUrl(url) 1054 | if albumId == "" { 1055 | fmt.Printf("Invalid URL: %s\n", url) 1056 | continue 1057 | } 1058 | err := rip(albumId, token, storefront) 1059 | if err != nil { 1060 | fmt.Println("Album failed.") 1061 | fmt.Println(err) 1062 | } 1063 | } 1064 | } 1065 | 1066 | func extractMedia(b string) (string, []string, error) { 1067 | masterUrl, err := url.Parse(b) 1068 | if err != nil { 1069 | return "", nil, err 1070 | } 1071 | resp, err := http.Get(b) 1072 | if err != nil { 1073 | return "", nil, err 1074 | } 1075 | defer resp.Body.Close() 1076 | if resp.StatusCode != http.StatusOK { 1077 | return "", nil, errors.New(resp.Status) 1078 | } 1079 | body, err := io.ReadAll(resp.Body) 1080 | if err != nil { 1081 | return "", nil, err 1082 | } 1083 | masterString := string(body) 1084 | from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) 1085 | if err != nil || listType != m3u8.MASTER { 1086 | return "", nil, errors.New("m3u8 not of master type") 1087 | } 1088 | master := from.(*m3u8.MasterPlaylist) 1089 | var streamUrl *url.URL 1090 | sort.Slice(master.Variants, func(i, j int) bool { 1091 | return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth 1092 | }) 1093 | for _, variant := range master.Variants { 1094 | if variant.Codecs == "alac" { 1095 | split := strings.Split(variant.Audio, "-") 1096 | length := len(split) 1097 | fmt.Printf("%s-bit / %s Hz\n", split[length-1], split[length-2]) 1098 | streamUrlTemp, err := masterUrl.Parse(variant.URI) 1099 | if err != nil { 1100 | panic(err) 1101 | } 1102 | streamUrl = streamUrlTemp 1103 | break 1104 | } 1105 | } 1106 | if streamUrl == nil { 1107 | return "", nil, errors.New("no alac codec found") 1108 | } 1109 | var keys []string 1110 | keys = append(keys, prefetchKey) 1111 | streamUrl.Path = strings.TrimSuffix(streamUrl.Path, ".m3u8") + "_m.mp4" 1112 | regex := regexp.MustCompile(`"(skd?://[^"]*)"`) 1113 | matches := regex.FindAllStringSubmatch(masterString, -1) 1114 | for _, match := range matches { 1115 | if strings.HasSuffix(match[1], "c23") || strings.HasSuffix(match[1], "c6") { 1116 | keys = append(keys, match[1]) 1117 | } 1118 | } 1119 | return streamUrl.String(), keys, nil 1120 | } 1121 | 1122 | func extractSong(url string) (*SongInfo, error) { 1123 | fmt.Println("Downloading...") 1124 | track, err := http.Get(url) 1125 | if err != nil { 1126 | return nil, err 1127 | } 1128 | defer track.Body.Close() 1129 | if track.StatusCode != http.StatusOK { 1130 | return nil, errors.New(track.Status) 1131 | } 1132 | rawSong, err := ioutil.ReadAll(track.Body) 1133 | if err != nil { 1134 | return nil, err 1135 | } 1136 | fmt.Println("Downloaded.") 1137 | f := bytes.NewReader(rawSong) 1138 | 1139 | trex, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ 1140 | mp4.BoxTypeMoov(), 1141 | mp4.BoxTypeMvex(), 1142 | mp4.BoxTypeTrex(), 1143 | }) 1144 | if err != nil || len(trex) != 1 { 1145 | return nil, err 1146 | } 1147 | trexPay := trex[0].Payload.(*mp4.Trex) 1148 | 1149 | stbl, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ 1150 | mp4.BoxTypeMoov(), 1151 | mp4.BoxTypeTrak(), 1152 | mp4.BoxTypeMdia(), 1153 | mp4.BoxTypeMinf(), 1154 | mp4.BoxTypeStbl(), 1155 | }) 1156 | if err != nil || len(stbl) != 1 { 1157 | return nil, err 1158 | } 1159 | 1160 | enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ 1161 | mp4.BoxTypeStsd(), 1162 | mp4.BoxTypeEnca(), 1163 | }) 1164 | if err != nil { 1165 | return nil, err 1166 | } 1167 | 1168 | aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, 1169 | []mp4.BoxType{BoxTypeAlac()}) 1170 | if err != nil || len(aalac) != 1 { 1171 | return nil, err 1172 | } 1173 | 1174 | extracted := &SongInfo{ 1175 | r: f, 1176 | alacParam: aalac[0].Payload.(*Alac), 1177 | } 1178 | 1179 | moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ 1180 | mp4.BoxTypeMoof(), 1181 | }) 1182 | if err != nil || len(moofs) <= 0 { 1183 | return nil, err 1184 | } 1185 | 1186 | mdats, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ 1187 | mp4.BoxTypeMdat(), 1188 | }) 1189 | if err != nil || len(mdats) != len(moofs) { 1190 | return nil, err 1191 | } 1192 | 1193 | for i, moof := range moofs { 1194 | tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ 1195 | mp4.BoxTypeTraf(), 1196 | mp4.BoxTypeTfhd(), 1197 | }) 1198 | if err != nil || len(tfhd) != 1 { 1199 | return nil, err 1200 | } 1201 | tfhdPay := tfhd[0].Payload.(*mp4.Tfhd) 1202 | index := tfhdPay.SampleDescriptionIndex 1203 | if index != 0 { 1204 | index-- 1205 | } 1206 | 1207 | truns, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ 1208 | mp4.BoxTypeTraf(), 1209 | mp4.BoxTypeTrun(), 1210 | }) 1211 | if err != nil || len(truns) <= 0 { 1212 | return nil, err 1213 | } 1214 | 1215 | mdat := mdats[i].Payload.(*mp4.Mdat).Data 1216 | for _, t := range truns { 1217 | for _, en := range t.Payload.(*mp4.Trun).Entries { 1218 | info := SampleInfo{descIndex: index} 1219 | 1220 | switch { 1221 | case t.Payload.CheckFlag(0x200): 1222 | info.data = mdat[:en.SampleSize] 1223 | mdat = mdat[en.SampleSize:] 1224 | case tfhdPay.CheckFlag(0x10): 1225 | info.data = mdat[:tfhdPay.DefaultSampleSize] 1226 | mdat = mdat[tfhdPay.DefaultSampleSize:] 1227 | default: 1228 | info.data = mdat[:trexPay.DefaultSampleSize] 1229 | mdat = mdat[trexPay.DefaultSampleSize:] 1230 | } 1231 | 1232 | switch { 1233 | case t.Payload.CheckFlag(0x100): 1234 | info.duration = en.SampleDuration 1235 | case tfhdPay.CheckFlag(0x8): 1236 | info.duration = tfhdPay.DefaultSampleDuration 1237 | default: 1238 | info.duration = trexPay.DefaultSampleDuration 1239 | } 1240 | 1241 | extracted.samples = append(extracted.samples, info) 1242 | } 1243 | } 1244 | if len(mdat) != 0 { 1245 | return nil, errors.New("offset mismatch") 1246 | } 1247 | } 1248 | 1249 | return extracted, nil 1250 | } 1251 | 1252 | func init() { 1253 | mp4.AddBoxDef((*Alac)(nil)) 1254 | } 1255 | 1256 | func BoxTypeAlac() mp4.BoxType { return mp4.StrToBoxType("alac") } 1257 | 1258 | type Alac struct { 1259 | mp4.FullBox `mp4:"extend"` 1260 | 1261 | FrameLength uint32 `mp4:"size=32"` 1262 | CompatibleVersion uint8 `mp4:"size=8"` 1263 | BitDepth uint8 `mp4:"size=8"` 1264 | Pb uint8 `mp4:"size=8"` 1265 | Mb uint8 `mp4:"size=8"` 1266 | Kb uint8 `mp4:"size=8"` 1267 | NumChannels uint8 `mp4:"size=8"` 1268 | MaxRun uint16 `mp4:"size=16"` 1269 | MaxFrameBytes uint32 `mp4:"size=32"` 1270 | AvgBitRate uint32 `mp4:"size=32"` 1271 | SampleRate uint32 `mp4:"size=32"` 1272 | } 1273 | 1274 | func getInfoFromAdam(adamId string, token string, storefront string) (*SongData, error) { 1275 | request, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, adamId), nil) 1276 | if err != nil { 1277 | return nil, err 1278 | } 1279 | query := url.Values{} 1280 | query.Set("extend", "extendedAssetUrls") 1281 | query.Set("include", "albums") 1282 | request.URL.RawQuery = query.Encode() 1283 | 1284 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1285 | 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)") 1286 | request.Header.Set("Origin", "https://music.apple.com") 1287 | 1288 | do, err := http.DefaultClient.Do(request) 1289 | if err != nil { 1290 | return nil, err 1291 | } 1292 | defer do.Body.Close() 1293 | if do.StatusCode != http.StatusOK { 1294 | return nil, errors.New(do.Status) 1295 | } 1296 | 1297 | obj := new(ApiResult) 1298 | err = json.NewDecoder(do.Body).Decode(&obj) 1299 | if err != nil { 1300 | return nil, err 1301 | } 1302 | 1303 | for _, d := range obj.Data { 1304 | if d.ID == adamId { 1305 | return &d, nil 1306 | } 1307 | } 1308 | return nil, nil 1309 | } 1310 | 1311 | func getToken() (string, error) { 1312 | req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) 1313 | if err != nil { 1314 | return "", err 1315 | } 1316 | 1317 | resp, err := http.DefaultClient.Do(req) 1318 | if err != nil { 1319 | return "", err 1320 | } 1321 | defer resp.Body.Close() 1322 | 1323 | body, err := io.ReadAll(resp.Body) 1324 | if err != nil { 1325 | return "", err 1326 | } 1327 | 1328 | regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`) 1329 | indexJsUri := regex.FindString(string(body)) 1330 | 1331 | req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil) 1332 | if err != nil { 1333 | return "", err 1334 | } 1335 | 1336 | resp, err = http.DefaultClient.Do(req) 1337 | if err != nil { 1338 | return "", err 1339 | } 1340 | defer resp.Body.Close() 1341 | 1342 | body, err = io.ReadAll(resp.Body) 1343 | if err != nil { 1344 | return "", err 1345 | } 1346 | 1347 | regex = regexp.MustCompile(`eyJh([^"]*)`) 1348 | token := regex.FindString(string(body)) 1349 | 1350 | return token, nil 1351 | } 1352 | 1353 | type ApiResult struct { 1354 | Data []SongData `json:"data"` 1355 | } 1356 | 1357 | type SongAttributes struct { 1358 | ArtistName string `json:"artistName"` 1359 | DiscNumber int `json:"discNumber"` 1360 | GenreNames []string `json:"genreNames"` 1361 | ExtendedAssetUrls struct { 1362 | EnhancedHls string `json:"enhancedHls"` 1363 | } `json:"extendedAssetUrls"` 1364 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1365 | ReleaseDate string `json:"releaseDate"` 1366 | Name string `json:"name"` 1367 | Isrc string `json:"isrc"` 1368 | AlbumName string `json:"albumName"` 1369 | TrackNumber int `json:"trackNumber"` 1370 | ComposerName string `json:"composerName"` 1371 | } 1372 | 1373 | type AlbumAttributes struct { 1374 | ArtistName string `json:"artistName"` 1375 | IsSingle bool `json:"isSingle"` 1376 | IsComplete bool `json:"isComplete"` 1377 | GenreNames []string `json:"genreNames"` 1378 | TrackCount int `json:"trackCount"` 1379 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1380 | ReleaseDate string `json:"releaseDate"` 1381 | Name string `json:"name"` 1382 | RecordLabel string `json:"recordLabel"` 1383 | Upc string `json:"upc"` 1384 | Copyright string `json:"copyright"` 1385 | IsCompilation bool `json:"isCompilation"` 1386 | } 1387 | 1388 | type SongData struct { 1389 | ID string `json:"id"` 1390 | Attributes SongAttributes `json:"attributes"` 1391 | Relationships struct { 1392 | Albums struct { 1393 | Data []struct { 1394 | ID string `json:"id"` 1395 | Type string `json:"type"` 1396 | Href string `json:"href"` 1397 | Attributes AlbumAttributes `json:"attributes"` 1398 | } `json:"data"` 1399 | } `json:"albums"` 1400 | Artists struct { 1401 | Href string `json:"href"` 1402 | Data []struct { 1403 | ID string `json:"id"` 1404 | Type string `json:"type"` 1405 | Href string `json:"href"` 1406 | } `json:"data"` 1407 | } `json:"artists"` 1408 | } `json:"relationships"` 1409 | } 1410 | 1411 | type SongResult struct { 1412 | Artwork struct { 1413 | Width int `json:"width"` 1414 | URL string `json:"url"` 1415 | Height int `json:"height"` 1416 | TextColor3 string `json:"textColor3"` 1417 | TextColor2 string `json:"textColor2"` 1418 | TextColor4 string `json:"textColor4"` 1419 | HasAlpha bool `json:"hasAlpha"` 1420 | TextColor1 string `json:"textColor1"` 1421 | BgColor string `json:"bgColor"` 1422 | HasP3 bool `json:"hasP3"` 1423 | SupportsLayeredImage bool `json:"supportsLayeredImage"` 1424 | } `json:"artwork"` 1425 | ArtistName string `json:"artistName"` 1426 | CollectionID string `json:"collectionId"` 1427 | DiscNumber int `json:"discNumber"` 1428 | GenreNames []string `json:"genreNames"` 1429 | ID string `json:"id"` 1430 | DurationInMillis int `json:"durationInMillis"` 1431 | ReleaseDate string `json:"releaseDate"` 1432 | ContentRatingsBySystem struct { 1433 | } `json:"contentRatingsBySystem"` 1434 | Name string `json:"name"` 1435 | Composer struct { 1436 | Name string `json:"name"` 1437 | URL string `json:"url"` 1438 | } `json:"composer"` 1439 | EditorialArtwork struct { 1440 | } `json:"editorialArtwork"` 1441 | CollectionName string `json:"collectionName"` 1442 | AssetUrls struct { 1443 | Plus string `json:"plus"` 1444 | Lightweight string `json:"lightweight"` 1445 | SuperLightweight string `json:"superLightweight"` 1446 | LightweightPlus string `json:"lightweightPlus"` 1447 | EnhancedHls string `json:"enhancedHls"` 1448 | } `json:"assetUrls"` 1449 | AudioTraits []string `json:"audioTraits"` 1450 | Kind string `json:"kind"` 1451 | Copyright string `json:"copyright"` 1452 | ArtistID string `json:"artistId"` 1453 | Genres []struct { 1454 | GenreID string `json:"genreId"` 1455 | Name string `json:"name"` 1456 | URL string `json:"url"` 1457 | MediaType string `json:"mediaType"` 1458 | } `json:"genres"` 1459 | TrackNumber int `json:"trackNumber"` 1460 | AudioLocale string `json:"audioLocale"` 1461 | Offers []struct { 1462 | ActionText struct { 1463 | Short string `json:"short"` 1464 | Medium string `json:"medium"` 1465 | Long string `json:"long"` 1466 | Downloaded string `json:"downloaded"` 1467 | Downloading string `json:"downloading"` 1468 | } `json:"actionText"` 1469 | Type string `json:"type"` 1470 | PriceFormatted string `json:"priceFormatted"` 1471 | Price float64 `json:"price"` 1472 | BuyParams string `json:"buyParams"` 1473 | Variant string `json:"variant,omitempty"` 1474 | Assets []struct { 1475 | Flavor string `json:"flavor"` 1476 | Preview struct { 1477 | Duration int `json:"duration"` 1478 | URL string `json:"url"` 1479 | } `json:"preview"` 1480 | Size int `json:"size"` 1481 | Duration int `json:"duration"` 1482 | } `json:"assets"` 1483 | } `json:"offers"` 1484 | } 1485 | type iTunesLookup struct { 1486 | Results map[string]SongResult `json:"results"` 1487 | } 1488 | 1489 | type Meta struct { 1490 | Context string `json:"@context"` 1491 | Type string `json:"@type"` 1492 | Name string `json:"name"` 1493 | Description string `json:"description"` 1494 | Tracks []struct { 1495 | Type string `json:"@type"` 1496 | Name string `json:"name"` 1497 | Audio struct { 1498 | Type string `json:"@type"` 1499 | } `json:"audio"` 1500 | Offers struct { 1501 | Type string `json:"@type"` 1502 | Category string `json:"category"` 1503 | Price int `json:"price"` 1504 | } `json:"offers"` 1505 | Duration string `json:"duration"` 1506 | } `json:"tracks"` 1507 | Citation []interface{} `json:"citation"` 1508 | WorkExample []struct { 1509 | Type string `json:"@type"` 1510 | Name string `json:"name"` 1511 | URL string `json:"url"` 1512 | Audio struct { 1513 | Type string `json:"@type"` 1514 | } `json:"audio"` 1515 | Offers struct { 1516 | Type string `json:"@type"` 1517 | Category string `json:"category"` 1518 | Price int `json:"price"` 1519 | } `json:"offers"` 1520 | Duration string `json:"duration"` 1521 | } `json:"workExample"` 1522 | Genre []string `json:"genre"` 1523 | DatePublished time.Time `json:"datePublished"` 1524 | ByArtist struct { 1525 | Type string `json:"@type"` 1526 | URL string `json:"url"` 1527 | Name string `json:"name"` 1528 | } `json:"byArtist"` 1529 | } 1530 | 1531 | type AutoGenerated struct { 1532 | Data []struct { 1533 | ID string `json:"id"` 1534 | Type string `json:"type"` 1535 | Href string `json:"href"` 1536 | Attributes struct { 1537 | Artwork struct { 1538 | Width int `json:"width"` 1539 | Height int `json:"height"` 1540 | URL string `json:"url"` 1541 | BgColor string `json:"bgColor"` 1542 | TextColor1 string `json:"textColor1"` 1543 | TextColor2 string `json:"textColor2"` 1544 | TextColor3 string `json:"textColor3"` 1545 | TextColor4 string `json:"textColor4"` 1546 | } `json:"artwork"` 1547 | ArtistName string `json:"artistName"` 1548 | IsSingle bool `json:"isSingle"` 1549 | URL string `json:"url"` 1550 | IsComplete bool `json:"isComplete"` 1551 | GenreNames []string `json:"genreNames"` 1552 | TrackCount int `json:"trackCount"` 1553 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1554 | ReleaseDate string `json:"releaseDate"` 1555 | Name string `json:"name"` 1556 | RecordLabel string `json:"recordLabel"` 1557 | Upc string `json:"upc"` 1558 | AudioTraits []string `json:"audioTraits"` 1559 | Copyright string `json:"copyright"` 1560 | PlayParams struct { 1561 | ID string `json:"id"` 1562 | Kind string `json:"kind"` 1563 | } `json:"playParams"` 1564 | IsCompilation bool `json:"isCompilation"` 1565 | } `json:"attributes"` 1566 | Relationships struct { 1567 | RecordLabels struct { 1568 | Href string `json:"href"` 1569 | Data []interface{} `json:"data"` 1570 | } `json:"record-labels"` 1571 | Artists struct { 1572 | Href string `json:"href"` 1573 | Data []struct { 1574 | ID string `json:"id"` 1575 | Type string `json:"type"` 1576 | Href string `json:"href"` 1577 | Attributes struct { 1578 | Name string `json:"name"` 1579 | } `json:"attributes"` 1580 | } `json:"data"` 1581 | } `json:"artists"` 1582 | Tracks struct { 1583 | Href string `json:"href"` 1584 | Data []struct { 1585 | ID string `json:"id"` 1586 | Type string `json:"type"` 1587 | Href string `json:"href"` 1588 | Attributes struct { 1589 | Previews []struct { 1590 | URL string `json:"url"` 1591 | } `json:"previews"` 1592 | Artwork struct { 1593 | Width int `json:"width"` 1594 | Height int `json:"height"` 1595 | URL string `json:"url"` 1596 | BgColor string `json:"bgColor"` 1597 | TextColor1 string `json:"textColor1"` 1598 | TextColor2 string `json:"textColor2"` 1599 | TextColor3 string `json:"textColor3"` 1600 | TextColor4 string `json:"textColor4"` 1601 | } `json:"artwork"` 1602 | ArtistName string `json:"artistName"` 1603 | URL string `json:"url"` 1604 | DiscNumber int `json:"discNumber"` 1605 | GenreNames []string `json:"genreNames"` 1606 | HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` 1607 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1608 | DurationInMillis int `json:"durationInMillis"` 1609 | ReleaseDate string `json:"releaseDate"` 1610 | Name string `json:"name"` 1611 | Isrc string `json:"isrc"` 1612 | AudioTraits []string `json:"audioTraits"` 1613 | HasLyrics bool `json:"hasLyrics"` 1614 | AlbumName string `json:"albumName"` 1615 | PlayParams struct { 1616 | ID string `json:"id"` 1617 | Kind string `json:"kind"` 1618 | } `json:"playParams"` 1619 | TrackNumber int `json:"trackNumber"` 1620 | AudioLocale string `json:"audioLocale"` 1621 | ComposerName string `json:"composerName"` 1622 | } `json:"attributes"` 1623 | Relationships struct { 1624 | Artists struct { 1625 | Href string `json:"href"` 1626 | Data []struct { 1627 | ID string `json:"id"` 1628 | Type string `json:"type"` 1629 | Href string `json:"href"` 1630 | Attributes struct { 1631 | Name string `json:"name"` 1632 | } `json:"attributes"` 1633 | } `json:"data"` 1634 | } `json:"artists"` 1635 | } `json:"relationships"` 1636 | } `json:"data"` 1637 | } `json:"tracks"` 1638 | } `json:"relationships"` 1639 | } `json:"data"` 1640 | } 1641 | -------------------------------------------------------------------------------- /main_atmos.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/ioutil" 11 | "math" 12 | "net" 13 | "net/http" 14 | "net/url" 15 | "os" 16 | "path/filepath" 17 | "regexp" 18 | "sort" 19 | "strings" 20 | "time" 21 | 22 | "github.com/abema/go-mp4" 23 | "github.com/grafov/m3u8" 24 | ) 25 | 26 | const ( 27 | defaultId = "0" 28 | prefetchKey = "skd://itunes.apple.com/P000000000/s1/e1" 29 | ) 30 | 31 | var ( 32 | forbiddenNames = regexp.MustCompile(`[/\\<>:"|?*]`) 33 | ) 34 | 35 | type SampleInfo struct { 36 | data []byte 37 | duration uint32 38 | descIndex uint32 39 | } 40 | 41 | type SongInfo struct { 42 | r io.ReadSeeker 43 | alacParam *Alac 44 | samples []SampleInfo 45 | } 46 | 47 | func (s *SongInfo) Duration() (ret uint64) { 48 | for i := range s.samples { 49 | ret += uint64(s.samples[i].duration) 50 | } 51 | return 52 | } 53 | 54 | func (*Alac) GetType() mp4.BoxType { 55 | return BoxTypeAlac() 56 | } 57 | 58 | func fileExists(path string) (bool, error) { 59 | f, err := os.Stat(path) 60 | if err == nil { 61 | return !f.IsDir(), nil 62 | } else if os.IsNotExist(err) { 63 | return false, nil 64 | } 65 | return false, err 66 | } 67 | 68 | func writeM4a(w *mp4.Writer, info *SongInfo, meta *AutoGenerated, data []byte, trackNum, trackTotal int) error { 69 | index := trackNum - 1 70 | { // ftyp 71 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeFtyp()}) 72 | if err != nil { 73 | return err 74 | } 75 | _, err = mp4.Marshal(w, &mp4.Ftyp{ 76 | MajorBrand: [4]byte{'M', '4', 'A', ' '}, 77 | MinorVersion: 0, 78 | CompatibleBrands: []mp4.CompatibleBrandElem{ 79 | {CompatibleBrand: [4]byte{'M', '4', 'A', ' '}}, 80 | {CompatibleBrand: [4]byte{'m', 'p', '4', '2'}}, 81 | {CompatibleBrand: mp4.BrandISOM()}, 82 | {CompatibleBrand: [4]byte{0, 0, 0, 0}}, 83 | }, 84 | }, box.Context) 85 | if err != nil { 86 | return err 87 | } 88 | _, err = w.EndBox() 89 | if err != nil { 90 | return err 91 | } 92 | } 93 | 94 | const chunkSize uint32 = 5 95 | duration := info.Duration() 96 | numSamples := uint32(len(info.samples)) 97 | var stco *mp4.BoxInfo 98 | 99 | { // moov 100 | _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMoov()}) 101 | if err != nil { 102 | return err 103 | } 104 | box, err := mp4.ExtractBox(info.r, nil, mp4.BoxPath{mp4.BoxTypeMoov()}) 105 | if err != nil { 106 | return err 107 | } 108 | moovOri := box[0] 109 | 110 | { // mvhd 111 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMvhd()}) 112 | if err != nil { 113 | return err 114 | } 115 | 116 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeMvhd()}) 117 | if err != nil { 118 | return err 119 | } 120 | mvhd := oriBox[0].Payload.(*mp4.Mvhd) 121 | if mvhd.Version == 0 { 122 | mvhd.DurationV0 = uint32(duration) 123 | } else { 124 | mvhd.DurationV1 = duration 125 | } 126 | 127 | _, err = mp4.Marshal(w, mvhd, oriBox[0].Info.Context) 128 | if err != nil { 129 | return err 130 | } 131 | 132 | _, err = w.EndBox() 133 | if err != nil { 134 | return err 135 | } 136 | } 137 | 138 | { // trak 139 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTrak()}) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | box, err := mp4.ExtractBox(info.r, moovOri, mp4.BoxPath{mp4.BoxTypeTrak()}) 145 | if err != nil { 146 | return err 147 | } 148 | trakOri := box[0] 149 | 150 | { // tkhd 151 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeTkhd()}) 152 | if err != nil { 153 | return err 154 | } 155 | 156 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeTkhd()}) 157 | if err != nil { 158 | return err 159 | } 160 | tkhd := oriBox[0].Payload.(*mp4.Tkhd) 161 | if tkhd.Version == 0 { 162 | tkhd.DurationV0 = uint32(duration) 163 | } else { 164 | tkhd.DurationV1 = duration 165 | } 166 | tkhd.SetFlags(0x7) 167 | 168 | _, err = mp4.Marshal(w, tkhd, oriBox[0].Info.Context) 169 | if err != nil { 170 | return err 171 | } 172 | 173 | _, err = w.EndBox() 174 | if err != nil { 175 | return err 176 | } 177 | } 178 | 179 | { // mdia 180 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdia()}) 181 | if err != nil { 182 | return err 183 | } 184 | 185 | box, err := mp4.ExtractBox(info.r, trakOri, mp4.BoxPath{mp4.BoxTypeMdia()}) 186 | if err != nil { 187 | return err 188 | } 189 | mdiaOri := box[0] 190 | 191 | { // mdhd 192 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdhd()}) 193 | if err != nil { 194 | return err 195 | } 196 | 197 | oriBox, err := mp4.ExtractBoxWithPayload(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMdhd()}) 198 | if err != nil { 199 | return err 200 | } 201 | mdhd := oriBox[0].Payload.(*mp4.Mdhd) 202 | if mdhd.Version == 0 { 203 | mdhd.DurationV0 = uint32(duration) 204 | } else { 205 | mdhd.DurationV1 = duration 206 | } 207 | 208 | _, err = mp4.Marshal(w, mdhd, oriBox[0].Info.Context) 209 | if err != nil { 210 | return err 211 | } 212 | 213 | _, err = w.EndBox() 214 | if err != nil { 215 | return err 216 | } 217 | } 218 | 219 | { // hdlr 220 | oriBox, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeHdlr()}) 221 | if err != nil { 222 | return err 223 | } 224 | 225 | err = w.CopyBox(info.r, oriBox[0]) 226 | if err != nil { 227 | return err 228 | } 229 | } 230 | 231 | { // minf 232 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMinf()}) 233 | if err != nil { 234 | return err 235 | } 236 | 237 | box, err := mp4.ExtractBox(info.r, mdiaOri, mp4.BoxPath{mp4.BoxTypeMinf()}) 238 | if err != nil { 239 | return err 240 | } 241 | minfOri := box[0] 242 | 243 | { // smhd, dinf 244 | boxes, err := mp4.ExtractBoxes(info.r, minfOri, []mp4.BoxPath{ 245 | {mp4.BoxTypeSmhd()}, 246 | {mp4.BoxTypeDinf()}, 247 | }) 248 | if err != nil { 249 | return err 250 | } 251 | 252 | for _, b := range boxes { 253 | err = w.CopyBox(info.r, b) 254 | if err != nil { 255 | return err 256 | } 257 | } 258 | } 259 | 260 | { // stbl 261 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStbl()}) 262 | if err != nil { 263 | return err 264 | } 265 | 266 | { // stsd 267 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsd()}) 268 | if err != nil { 269 | return err 270 | } 271 | _, err = mp4.Marshal(w, &mp4.Stsd{EntryCount: 1}, box.Context) 272 | if err != nil { 273 | return err 274 | } 275 | 276 | { // alac 277 | _, err = w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) 278 | if err != nil { 279 | return err 280 | } 281 | 282 | _, err = w.Write([]byte{ 283 | 0, 0, 0, 0, 0, 0, 0, 1, 284 | 0, 0, 0, 0, 0, 0, 0, 0}) 285 | if err != nil { 286 | return err 287 | } 288 | 289 | err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.NumChannels)) 290 | if err != nil { 291 | return err 292 | } 293 | 294 | err = binary.Write(w, binary.BigEndian, uint16(info.alacParam.BitDepth)) 295 | if err != nil { 296 | return err 297 | } 298 | 299 | _, err = w.Write([]byte{0, 0}) 300 | if err != nil { 301 | return err 302 | } 303 | 304 | err = binary.Write(w, binary.BigEndian, info.alacParam.SampleRate) 305 | if err != nil { 306 | return err 307 | } 308 | 309 | _, err = w.Write([]byte{0, 0}) 310 | if err != nil { 311 | return err 312 | } 313 | 314 | box, err := w.StartBox(&mp4.BoxInfo{Type: BoxTypeAlac()}) 315 | if err != nil { 316 | return err 317 | } 318 | 319 | _, err = mp4.Marshal(w, info.alacParam, box.Context) 320 | if err != nil { 321 | return err 322 | } 323 | 324 | _, err = w.EndBox() 325 | if err != nil { 326 | return err 327 | } 328 | 329 | _, err = w.EndBox() 330 | if err != nil { 331 | return err 332 | } 333 | } 334 | 335 | _, err = w.EndBox() 336 | if err != nil { 337 | return err 338 | } 339 | } 340 | 341 | { // stts 342 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStts()}) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | var stts mp4.Stts 348 | for _, sample := range info.samples { 349 | if len(stts.Entries) != 0 { 350 | last := &stts.Entries[len(stts.Entries)-1] 351 | if last.SampleDelta == sample.duration { 352 | last.SampleCount++ 353 | continue 354 | } 355 | } 356 | stts.Entries = append(stts.Entries, mp4.SttsEntry{ 357 | SampleCount: 1, 358 | SampleDelta: sample.duration, 359 | }) 360 | } 361 | stts.EntryCount = uint32(len(stts.Entries)) 362 | 363 | _, err = mp4.Marshal(w, &stts, box.Context) 364 | if err != nil { 365 | return err 366 | } 367 | 368 | _, err = w.EndBox() 369 | if err != nil { 370 | return err 371 | } 372 | } 373 | 374 | { // stsc 375 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsc()}) 376 | if err != nil { 377 | return err 378 | } 379 | 380 | if numSamples%chunkSize == 0 { 381 | _, err = mp4.Marshal(w, &mp4.Stsc{ 382 | EntryCount: 1, 383 | Entries: []mp4.StscEntry{ 384 | { 385 | FirstChunk: 1, 386 | SamplesPerChunk: chunkSize, 387 | SampleDescriptionIndex: 1, 388 | }, 389 | }, 390 | }, box.Context) 391 | } else { 392 | _, err = mp4.Marshal(w, &mp4.Stsc{ 393 | EntryCount: 2, 394 | Entries: []mp4.StscEntry{ 395 | { 396 | FirstChunk: 1, 397 | SamplesPerChunk: chunkSize, 398 | SampleDescriptionIndex: 1, 399 | }, { 400 | FirstChunk: numSamples/chunkSize + 1, 401 | SamplesPerChunk: numSamples % chunkSize, 402 | SampleDescriptionIndex: 1, 403 | }, 404 | }, 405 | }, box.Context) 406 | } 407 | 408 | _, err = w.EndBox() 409 | if err != nil { 410 | return err 411 | } 412 | } 413 | 414 | { // stsz 415 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStsz()}) 416 | if err != nil { 417 | return err 418 | } 419 | 420 | stsz := mp4.Stsz{SampleCount: numSamples} 421 | for _, sample := range info.samples { 422 | stsz.EntrySize = append(stsz.EntrySize, uint32(len(sample.data))) 423 | } 424 | 425 | _, err = mp4.Marshal(w, &stsz, box.Context) 426 | if err != nil { 427 | return err 428 | } 429 | 430 | _, err = w.EndBox() 431 | if err != nil { 432 | return err 433 | } 434 | } 435 | 436 | { // stco 437 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeStco()}) 438 | if err != nil { 439 | return err 440 | } 441 | 442 | l := (numSamples + chunkSize - 1) / chunkSize 443 | _, err = mp4.Marshal(w, &mp4.Stco{ 444 | EntryCount: l, 445 | ChunkOffset: make([]uint32, l), 446 | }, box.Context) 447 | 448 | stco, err = w.EndBox() 449 | if err != nil { 450 | return err 451 | } 452 | } 453 | 454 | _, err = w.EndBox() 455 | if err != nil { 456 | return err 457 | } 458 | } 459 | 460 | _, err = w.EndBox() 461 | if err != nil { 462 | return err 463 | } 464 | } 465 | 466 | _, err = w.EndBox() 467 | if err != nil { 468 | return err 469 | } 470 | } 471 | 472 | _, err = w.EndBox() 473 | if err != nil { 474 | return err 475 | } 476 | } 477 | 478 | { // udta 479 | ctx := mp4.Context{UnderUdta: true} 480 | _, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeUdta(), Context: ctx}) 481 | if err != nil { 482 | return err 483 | } 484 | 485 | { // meta 486 | ctx.UnderIlstMeta = true 487 | 488 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMeta(), Context: ctx}) 489 | if err != nil { 490 | return err 491 | } 492 | 493 | _, err = mp4.Marshal(w, &mp4.Meta{}, ctx) 494 | if err != nil { 495 | return err 496 | } 497 | 498 | { // hdlr 499 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeHdlr(), Context: ctx}) 500 | if err != nil { 501 | return err 502 | } 503 | 504 | _, err = mp4.Marshal(w, &mp4.Hdlr{ 505 | HandlerType: [4]byte{'m', 'd', 'i', 'r'}, 506 | Reserved: [3]uint32{0x6170706c, 0, 0}, 507 | }, ctx) 508 | if err != nil { 509 | return err 510 | } 511 | 512 | _, err = w.EndBox() 513 | if err != nil { 514 | return err 515 | } 516 | } 517 | 518 | { // ilst 519 | ctx.UnderIlst = true 520 | 521 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeIlst(), Context: ctx}) 522 | if err != nil { 523 | return err 524 | } 525 | 526 | marshalData := func(val interface{}) error { 527 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeData()}) 528 | if err != nil { 529 | return err 530 | } 531 | 532 | var boxData mp4.Data 533 | switch v := val.(type) { 534 | case string: 535 | boxData.DataType = mp4.DataTypeStringUTF8 536 | boxData.Data = []byte(v) 537 | case uint8: 538 | boxData.DataType = mp4.DataTypeSignedIntBigEndian 539 | boxData.Data = []byte{v} 540 | case uint32: 541 | boxData.DataType = mp4.DataTypeSignedIntBigEndian 542 | boxData.Data = make([]byte, 4) 543 | binary.BigEndian.PutUint32(boxData.Data, v) 544 | case []byte: 545 | boxData.DataType = mp4.DataTypeBinary 546 | boxData.Data = v 547 | default: 548 | panic("unsupported value") 549 | } 550 | 551 | _, err = mp4.Marshal(w, &boxData, ctx) 552 | if err != nil { 553 | return err 554 | } 555 | 556 | _, err = w.EndBox() 557 | return err 558 | } 559 | 560 | addMeta := func(tag mp4.BoxType, val interface{}) error { 561 | _, err = w.StartBox(&mp4.BoxInfo{Type: tag}) 562 | if err != nil { 563 | return err 564 | } 565 | 566 | err = marshalData(val) 567 | if err != nil { 568 | return err 569 | } 570 | 571 | _, err = w.EndBox() 572 | return err 573 | } 574 | 575 | addExtendedMeta := func(name string, val interface{}) error { 576 | ctx.UnderIlstFreeMeta = true 577 | 578 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'-', '-', '-', '-'}, Context: ctx}) 579 | if err != nil { 580 | return err 581 | } 582 | 583 | { 584 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'m', 'e', 'a', 'n'}, Context: ctx}) 585 | if err != nil { 586 | return err 587 | } 588 | 589 | _, err = w.Write([]byte{0, 0, 0, 0}) 590 | if err != nil { 591 | return err 592 | } 593 | 594 | _, err = io.WriteString(w, "com.apple.iTunes") 595 | if err != nil { 596 | return err 597 | } 598 | 599 | _, err = w.EndBox() 600 | if err != nil { 601 | return err 602 | } 603 | } 604 | 605 | { 606 | _, err = w.StartBox(&mp4.BoxInfo{Type: mp4.BoxType{'n', 'a', 'm', 'e'}, Context: ctx}) 607 | if err != nil { 608 | return err 609 | } 610 | 611 | _, err = w.Write([]byte{0, 0, 0, 0}) 612 | if err != nil { 613 | return err 614 | } 615 | 616 | _, err = io.WriteString(w, name) 617 | if err != nil { 618 | return err 619 | } 620 | 621 | _, err = w.EndBox() 622 | if err != nil { 623 | return err 624 | } 625 | } 626 | 627 | err = marshalData(val) 628 | if err != nil { 629 | return err 630 | } 631 | 632 | ctx.UnderIlstFreeMeta = false 633 | 634 | _, err = w.EndBox() 635 | return err 636 | } 637 | 638 | err = addMeta(mp4.BoxType{'\251', 'n', 'a', 'm'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.Name) 639 | if err != nil { 640 | return err 641 | } 642 | 643 | err = addMeta(mp4.BoxType{'\251', 'a', 'l', 'b'}, meta.Data[0].Attributes.Name) 644 | if err != nil { 645 | return err 646 | } 647 | 648 | err = addMeta(mp4.BoxType{'\251', 'A', 'R', 'T'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ArtistName) 649 | if err != nil { 650 | return err 651 | } 652 | 653 | err = addMeta(mp4.BoxType{'\251', 'w', 'r', 't'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.ComposerName) 654 | if err != nil { 655 | return err 656 | } 657 | 658 | err = addMeta(mp4.BoxType{'\251', 'd', 'a', 'y'}, strings.Split(meta.Data[0].Attributes.ReleaseDate, "-")[0]) 659 | if err != nil { 660 | return err 661 | } 662 | 663 | // cnID, err := strconv.ParseUint(meta.Data[0].Relationships.Tracks.Data[index].ID, 10, 32) 664 | // if err != nil { 665 | // return err 666 | // } 667 | 668 | // err = addMeta(mp4.BoxType{'c', 'n', 'I', 'D'}, uint32(cnID)) 669 | // if err != nil { 670 | // return err 671 | // } 672 | 673 | err = addExtendedMeta("ISRC", meta.Data[0].Relationships.Tracks.Data[index].Attributes.Isrc) 674 | if err != nil { 675 | return err 676 | } 677 | 678 | if len(meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames) > 0 { 679 | err = addMeta(mp4.BoxType{'\251', 'g', 'e', 'n'}, meta.Data[0].Relationships.Tracks.Data[index].Attributes.GenreNames[0]) 680 | if err != nil { 681 | return err 682 | } 683 | } 684 | 685 | if len(meta.Data) > 0 { 686 | album := meta.Data[0] 687 | 688 | err = addMeta(mp4.BoxType{'a', 'A', 'R', 'T'}, album.Attributes.ArtistName) 689 | if err != nil { 690 | return err 691 | } 692 | 693 | err = addMeta(mp4.BoxType{'c', 'p', 'r', 't'}, album.Attributes.Copyright) 694 | if err != nil { 695 | return err 696 | } 697 | 698 | var isCpil uint8 699 | if album.Attributes.IsCompilation { 700 | isCpil = 1 701 | } 702 | err = addMeta(mp4.BoxType{'c', 'p', 'i', 'l'}, isCpil) 703 | if err != nil { 704 | return err 705 | } 706 | 707 | err = addExtendedMeta("LABEL", album.Attributes.RecordLabel) 708 | if err != nil { 709 | return err 710 | } 711 | 712 | err = addExtendedMeta("UPC", album.Attributes.Upc) 713 | if err != nil { 714 | return err 715 | } 716 | 717 | // plID, err := strconv.ParseUint(album.ID, 10, 32) 718 | // if err != nil { 719 | // return err 720 | // } 721 | 722 | // err = addMeta(mp4.BoxType{'p', 'l', 'I', 'D'}, uint32(plID)) 723 | // if err != nil { 724 | // return err 725 | // } 726 | } 727 | 728 | // if len(meta.Data[0].Relationships.Artists.Data) > 0 { 729 | // atID, err := strconv.ParseUint(meta.Data[0].Relationships.Artists.Data[index].ID, 10, 32) 730 | // if err != nil { 731 | // return err 732 | // } 733 | 734 | // err = addMeta(mp4.BoxType{'a', 't', 'I', 'D'}, uint32(atID)) 735 | // if err != nil { 736 | // return err 737 | // } 738 | // } 739 | 740 | trkn := make([]byte, 8) 741 | binary.BigEndian.PutUint32(trkn, uint32(trackNum)) 742 | binary.BigEndian.PutUint16(trkn[4:], uint16(trackTotal)) 743 | err = addMeta(mp4.BoxType{'t', 'r', 'k', 'n'}, trkn) 744 | if err != nil { 745 | return err 746 | } 747 | 748 | // disk := make([]byte, 8) 749 | // binary.BigEndian.PutUint32(disk, uint32(meta.Attributes.DiscNumber)) 750 | // err = addMeta(mp4.BoxType{'d', 'i', 's', 'k'}, disk) 751 | // if err != nil { 752 | // return err 753 | // } 754 | 755 | ctx.UnderIlst = false 756 | 757 | _, err = w.EndBox() 758 | if err != nil { 759 | return err 760 | } 761 | } 762 | 763 | ctx.UnderIlstMeta = false 764 | _, err = w.EndBox() 765 | if err != nil { 766 | return err 767 | } 768 | } 769 | 770 | ctx.UnderUdta = false 771 | _, err = w.EndBox() 772 | if err != nil { 773 | return err 774 | } 775 | } 776 | 777 | _, err = w.EndBox() 778 | if err != nil { 779 | return err 780 | } 781 | } 782 | 783 | { 784 | box, err := w.StartBox(&mp4.BoxInfo{Type: mp4.BoxTypeMdat()}) 785 | if err != nil { 786 | return err 787 | } 788 | 789 | _, err = mp4.Marshal(w, &mp4.Mdat{Data: data}, box.Context) 790 | if err != nil { 791 | return err 792 | } 793 | 794 | mdat, err := w.EndBox() 795 | 796 | var realStco mp4.Stco 797 | 798 | offset := mdat.Offset + mdat.HeaderSize 799 | for i := uint32(0); i < numSamples; i++ { 800 | if i%chunkSize == 0 { 801 | realStco.EntryCount++ 802 | realStco.ChunkOffset = append(realStco.ChunkOffset, uint32(offset)) 803 | } 804 | offset += uint64(len(info.samples[i].data)) 805 | } 806 | 807 | _, err = stco.SeekToPayload(w) 808 | if err != nil { 809 | return err 810 | } 811 | _, err = mp4.Marshal(w, &realStco, box.Context) 812 | if err != nil { 813 | return err 814 | } 815 | } 816 | 817 | return nil 818 | } 819 | 820 | func decryptSong(info *SongInfo, keys []string, manifest *AutoGenerated, filename string, trackNum, trackTotal int) error { 821 | //fmt.Printf("%d-bit / %d Hz\n", info.bitDepth, info.bitRate) 822 | conn, err := net.Dial("tcp", "127.0.0.1:10020") 823 | if err != nil { 824 | return err 825 | } 826 | defer conn.Close() 827 | var decrypted []byte 828 | var lastIndex uint32 = math.MaxUint8 829 | 830 | fmt.Println("Decrypt start.") 831 | for _, sp := range info.samples { 832 | if lastIndex != sp.descIndex { 833 | if len(decrypted) != 0 { 834 | _, err := conn.Write([]byte{0, 0, 0, 0}) 835 | if err != nil { 836 | return err 837 | } 838 | } 839 | keyUri := keys[sp.descIndex] 840 | id := manifest.Data[0].Relationships.Tracks.Data[trackNum-1].ID 841 | if keyUri == prefetchKey { 842 | id = defaultId 843 | } 844 | 845 | _, err := conn.Write([]byte{byte(len(id))}) 846 | if err != nil { 847 | return err 848 | } 849 | _, err = io.WriteString(conn, id) 850 | if err != nil { 851 | return err 852 | } 853 | 854 | _, err = conn.Write([]byte{byte(len(keyUri))}) 855 | if err != nil { 856 | return err 857 | } 858 | _, err = io.WriteString(conn, keyUri) 859 | if err != nil { 860 | return err 861 | } 862 | } 863 | lastIndex = sp.descIndex 864 | 865 | err := binary.Write(conn, binary.LittleEndian, uint32(len(sp.data))) 866 | if err != nil { 867 | return err 868 | } 869 | 870 | _, err = conn.Write(sp.data) 871 | if err != nil { 872 | return err 873 | } 874 | 875 | de := make([]byte, len(sp.data)) 876 | _, err = io.ReadFull(conn, de) 877 | if err != nil { 878 | return err 879 | } 880 | 881 | decrypted = append(decrypted, de...) 882 | } 883 | _, _ = conn.Write([]byte{0, 0, 0, 0, 0}) 884 | 885 | fmt.Println("Decrypt finished.") 886 | 887 | file, err := os.Create(filename) 888 | if err != nil { 889 | panic(err) 890 | } 891 | defer file.Close() 892 | 893 | _, err = file.Write(decrypted) 894 | if err != nil { 895 | panic(err) 896 | } 897 | 898 | return nil 899 | // return writeM4a(mp4.NewWriter(create), info, manifest, decrypted, trackNum, trackTotal) 900 | } 901 | 902 | func checkUrl(url string) (string, string) { 903 | pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/album|\/album\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) 904 | matches := pat.FindAllStringSubmatch(url, -1) 905 | if matches == nil { 906 | return "", "" 907 | } else { 908 | return matches[0][1], matches[0][2] 909 | } 910 | } 911 | 912 | func getMeta(albumId string, token string, storefront string) (*AutoGenerated, error) { 913 | req, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/albums/%s", storefront, albumId), nil) 914 | if err != nil { 915 | return nil, err 916 | } 917 | req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 918 | 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") 919 | req.Header.Set("Origin", "https://music.apple.com") 920 | query := url.Values{} 921 | query.Set("omit[resource]", "autos") 922 | query.Set("include", "tracks,artists,record-labels") 923 | query.Set("include[songs]", "artists") 924 | query.Set("fields[artists]", "name") 925 | query.Set("fields[albums:albums]", "artistName,artwork,name,releaseDate,url") 926 | query.Set("fields[record-labels]", "name") 927 | // query.Set("l", "en-gb") 928 | req.URL.RawQuery = query.Encode() 929 | do, err := http.DefaultClient.Do(req) 930 | if err != nil { 931 | return nil, err 932 | } 933 | defer do.Body.Close() 934 | if do.StatusCode != http.StatusOK { 935 | return nil, errors.New(do.Status) 936 | } 937 | obj := new(AutoGenerated) 938 | err = json.NewDecoder(do.Body).Decode(&obj) 939 | if err != nil { 940 | return nil, err 941 | } 942 | return obj, nil 943 | } 944 | 945 | func writeCover(sanAlbumFolder, url string) error { 946 | covPath := filepath.Join(sanAlbumFolder, "cover.jpg") 947 | exists, err := fileExists(covPath) 948 | if err != nil { 949 | fmt.Println("Failed to check if cover exists.") 950 | return err 951 | } 952 | if exists { 953 | return nil 954 | } 955 | url = strings.Replace(url, "{w}x{h}", "1200x12000", 1) 956 | req, err := http.NewRequest("GET", url, nil) 957 | if err != nil { 958 | return err 959 | } 960 | 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") 961 | do, err := http.DefaultClient.Do(req) 962 | if err != nil { 963 | return err 964 | } 965 | defer do.Body.Close() 966 | if do.StatusCode != http.StatusOK { 967 | errors.New(do.Status) 968 | } 969 | f, err := os.Create(covPath) 970 | if err != nil { 971 | return err 972 | } 973 | defer f.Close() 974 | _, err = io.Copy(f, do.Body) 975 | if err != nil { 976 | return err 977 | } 978 | return nil 979 | } 980 | 981 | func rip(albumId string, token string, storefront string) error { 982 | meta, err := getMeta(albumId, token, storefront) 983 | if err != nil { 984 | fmt.Println("Failed to get album metadata.\n") 985 | return err 986 | } 987 | albumFolder := fmt.Sprintf("%s - %s", meta.Data[0].Attributes.ArtistName, meta.Data[0].Attributes.Name) 988 | sanAlbumFolder := filepath.Join("AM-DL downloads", forbiddenNames.ReplaceAllString(albumFolder, "_")) 989 | os.MkdirAll(sanAlbumFolder, os.ModePerm) 990 | fmt.Println(albumFolder) 991 | err = writeCover(sanAlbumFolder, meta.Data[0].Attributes.Artwork.URL) 992 | if err != nil { 993 | fmt.Println("Failed to write cover.") 994 | } 995 | trackTotal := len(meta.Data[0].Relationships.Tracks.Data) 996 | for trackNum, track := range meta.Data[0].Relationships.Tracks.Data { 997 | trackNum++ 998 | fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) 999 | manifest, err := getInfoFromAdam(track.ID, token, storefront) 1000 | if err != nil { 1001 | fmt.Println("Failed to get manifest.\n", err) 1002 | continue 1003 | } 1004 | if manifest.Attributes.ExtendedAssetUrls.EnhancedHls == "" { 1005 | fmt.Println("Unavailable in ALAC.") 1006 | continue 1007 | } 1008 | filename := fmt.Sprintf("%02d. %s.ec3", trackNum, forbiddenNames.ReplaceAllString(track.Attributes.Name, "_")) 1009 | trackPath := filepath.Join(sanAlbumFolder, filename) 1010 | exists, err := fileExists(trackPath) 1011 | if err != nil { 1012 | fmt.Println("Failed to check if track exists.") 1013 | } 1014 | if exists { 1015 | fmt.Println("Track already exists locally.") 1016 | continue 1017 | } 1018 | trackUrl, keys, err := extractMedia(manifest.Attributes.ExtendedAssetUrls.EnhancedHls) 1019 | if err != nil { 1020 | fmt.Println("Failed to extract info from manifest.\n", err) 1021 | continue 1022 | } 1023 | info, err := extractSong(trackUrl) 1024 | if err != nil { 1025 | fmt.Println("Failed to extract track.", err) 1026 | continue 1027 | } 1028 | samplesOk := true 1029 | for samplesOk { 1030 | for _, i := range info.samples { 1031 | if int(i.descIndex) >= len(keys) { 1032 | fmt.Println("Decryption size mismatch.") 1033 | samplesOk = false 1034 | } 1035 | } 1036 | break 1037 | } 1038 | if !samplesOk { 1039 | continue 1040 | } 1041 | err = decryptSong(info, keys, meta, trackPath, trackNum, trackTotal) 1042 | if err != nil { 1043 | fmt.Println("Failed to decrypt track.\n", err) 1044 | continue 1045 | } 1046 | } 1047 | return err 1048 | } 1049 | 1050 | func main() { 1051 | token, err := getToken() 1052 | if err != nil { 1053 | fmt.Println("Failed to get token.") 1054 | return 1055 | } 1056 | albumTotal := len(os.Args[1:]) 1057 | for albumNum, url := range os.Args[1:] { 1058 | fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) 1059 | storefront, albumId := checkUrl(url) 1060 | if albumId == "" { 1061 | fmt.Printf("Invalid URL: %s\n", url) 1062 | continue 1063 | } 1064 | err := rip(albumId, token, storefront) 1065 | if err != nil { 1066 | fmt.Println("Album failed.") 1067 | fmt.Println(err) 1068 | } 1069 | } 1070 | } 1071 | 1072 | func extractMedia(b string) (string, []string, error) { 1073 | masterUrl, err := url.Parse(b) 1074 | if err != nil { 1075 | return "", nil, err 1076 | } 1077 | resp, err := http.Get(b) 1078 | if err != nil { 1079 | return "", nil, err 1080 | } 1081 | defer resp.Body.Close() 1082 | if resp.StatusCode != http.StatusOK { 1083 | return "", nil, errors.New(resp.Status) 1084 | } 1085 | body, err := io.ReadAll(resp.Body) 1086 | if err != nil { 1087 | return "", nil, err 1088 | } 1089 | masterString := string(body) 1090 | from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) 1091 | if err != nil || listType != m3u8.MASTER { 1092 | return "", nil, errors.New("m3u8 not of master type") 1093 | } 1094 | master := from.(*m3u8.MasterPlaylist) 1095 | var streamUrl *url.URL 1096 | sort.Slice(master.Variants, func(i, j int) bool { 1097 | return master.Variants[i].AverageBandwidth > master.Variants[j].AverageBandwidth 1098 | }) 1099 | for _, variant := range master.Variants { 1100 | if variant.Codecs == "ec-3" { 1101 | fmt.Printf("%s\n", variant.Audio) 1102 | streamUrlTemp, err := masterUrl.Parse(variant.URI) 1103 | if err != nil { 1104 | panic(err) 1105 | } 1106 | streamUrl = streamUrlTemp 1107 | break 1108 | } 1109 | } 1110 | if streamUrl == nil { 1111 | return "", nil, errors.New("no ec-3 codec found") 1112 | } 1113 | var keys []string 1114 | keys = append(keys, prefetchKey) 1115 | streamUrl.Path = strings.TrimSuffix(streamUrl.Path, ".m3u8") + "_m.mp4" 1116 | regex := regexp.MustCompile(`"(skd?://[^"]*)"`) 1117 | matches := regex.FindAllStringSubmatch(masterString, -1) 1118 | for _, match := range matches { 1119 | if strings.HasSuffix(match[1], "c24") || strings.HasSuffix(match[1], "c6") { 1120 | keys = append(keys, match[1]) 1121 | } 1122 | } 1123 | return streamUrl.String(), keys, nil 1124 | } 1125 | 1126 | func extractSong(url string) (*SongInfo, error) { 1127 | fmt.Println("Downloading...") 1128 | track, err := http.Get(url) 1129 | if err != nil { 1130 | return nil, err 1131 | } 1132 | defer track.Body.Close() 1133 | if track.StatusCode != http.StatusOK { 1134 | return nil, errors.New(track.Status) 1135 | } 1136 | rawSong, err := ioutil.ReadAll(track.Body) 1137 | if err != nil { 1138 | return nil, err 1139 | } 1140 | fmt.Println("Downloaded.") 1141 | 1142 | f := bytes.NewReader(rawSong) 1143 | 1144 | trex, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ 1145 | mp4.BoxTypeMoov(), 1146 | mp4.BoxTypeMvex(), 1147 | mp4.BoxTypeTrex(), 1148 | }) 1149 | if err != nil || len(trex) != 1 { 1150 | return nil, err 1151 | } 1152 | trexPay := trex[0].Payload.(*mp4.Trex) 1153 | 1154 | stbl, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ 1155 | mp4.BoxTypeMoov(), 1156 | mp4.BoxTypeTrak(), 1157 | mp4.BoxTypeMdia(), 1158 | mp4.BoxTypeMinf(), 1159 | mp4.BoxTypeStbl(), 1160 | }) 1161 | if err != nil || len(stbl) != 1 { 1162 | return nil, err 1163 | } 1164 | 1165 | // enca, err := mp4.ExtractBoxWithPayload(f, stbl[0], []mp4.BoxType{ 1166 | // mp4.BoxTypeStsd(), 1167 | // mp4.BoxTypeEnca(), 1168 | // }) 1169 | // if err != nil { 1170 | // return nil, err 1171 | // } 1172 | 1173 | // aalac, err := mp4.ExtractBoxWithPayload(f, &enca[0].Info, 1174 | // []mp4.BoxType{mp4.StrToBoxType("dec3")}) 1175 | // if err != nil || len(aalac) != 1 { 1176 | // return nil, err 1177 | // } 1178 | 1179 | extracted := &SongInfo{ 1180 | r: f, 1181 | // alacParam: aalac[0].Payload.(*Alac), 1182 | } 1183 | 1184 | moofs, err := mp4.ExtractBox(f, nil, []mp4.BoxType{ 1185 | mp4.BoxTypeMoof(), 1186 | }) 1187 | if err != nil || len(moofs) <= 0 { 1188 | return nil, err 1189 | } 1190 | 1191 | mdats, err := mp4.ExtractBoxWithPayload(f, nil, []mp4.BoxType{ 1192 | mp4.BoxTypeMdat(), 1193 | }) 1194 | if err != nil || len(mdats) != len(moofs) { 1195 | return nil, err 1196 | } 1197 | 1198 | for i, moof := range moofs { 1199 | tfhd, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ 1200 | mp4.BoxTypeTraf(), 1201 | mp4.BoxTypeTfhd(), 1202 | }) 1203 | if err != nil || len(tfhd) != 1 { 1204 | return nil, err 1205 | } 1206 | tfhdPay := tfhd[0].Payload.(*mp4.Tfhd) 1207 | index := tfhdPay.SampleDescriptionIndex 1208 | if index != 0 { 1209 | index-- 1210 | } 1211 | 1212 | truns, err := mp4.ExtractBoxWithPayload(f, moof, []mp4.BoxType{ 1213 | mp4.BoxTypeTraf(), 1214 | mp4.BoxTypeTrun(), 1215 | }) 1216 | if err != nil || len(truns) <= 0 { 1217 | return nil, err 1218 | } 1219 | 1220 | mdat := mdats[i].Payload.(*mp4.Mdat).Data 1221 | for _, t := range truns { 1222 | for _, en := range t.Payload.(*mp4.Trun).Entries { 1223 | info := SampleInfo{descIndex: index} 1224 | 1225 | switch { 1226 | case t.Payload.CheckFlag(0x200): 1227 | info.data = mdat[:en.SampleSize] 1228 | mdat = mdat[en.SampleSize:] 1229 | case tfhdPay.CheckFlag(0x10): 1230 | info.data = mdat[:tfhdPay.DefaultSampleSize] 1231 | mdat = mdat[tfhdPay.DefaultSampleSize:] 1232 | default: 1233 | info.data = mdat[:trexPay.DefaultSampleSize] 1234 | mdat = mdat[trexPay.DefaultSampleSize:] 1235 | } 1236 | 1237 | switch { 1238 | case t.Payload.CheckFlag(0x100): 1239 | info.duration = en.SampleDuration 1240 | case tfhdPay.CheckFlag(0x8): 1241 | info.duration = tfhdPay.DefaultSampleDuration 1242 | default: 1243 | info.duration = trexPay.DefaultSampleDuration 1244 | } 1245 | 1246 | extracted.samples = append(extracted.samples, info) 1247 | } 1248 | } 1249 | if len(mdat) != 0 { 1250 | return nil, errors.New("offset mismatch") 1251 | } 1252 | } 1253 | 1254 | return extracted, nil 1255 | } 1256 | 1257 | func init() { 1258 | mp4.AddBoxDef((*Alac)(nil)) 1259 | } 1260 | 1261 | func BoxTypeAlac() mp4.BoxType { return mp4.StrToBoxType("alac") } 1262 | 1263 | type Alac struct { 1264 | mp4.FullBox `mp4:"extend"` 1265 | 1266 | FrameLength uint32 `mp4:"size=32"` 1267 | CompatibleVersion uint8 `mp4:"size=8"` 1268 | BitDepth uint8 `mp4:"size=8"` 1269 | Pb uint8 `mp4:"size=8"` 1270 | Mb uint8 `mp4:"size=8"` 1271 | Kb uint8 `mp4:"size=8"` 1272 | NumChannels uint8 `mp4:"size=8"` 1273 | MaxRun uint16 `mp4:"size=16"` 1274 | MaxFrameBytes uint32 `mp4:"size=32"` 1275 | AvgBitRate uint32 `mp4:"size=32"` 1276 | SampleRate uint32 `mp4:"size=32"` 1277 | } 1278 | 1279 | func getInfoFromAdam(adamId string, token string, storefront string) (*SongData, error) { 1280 | request, err := http.NewRequest("GET", fmt.Sprintf("https://amp-api.music.apple.com/v1/catalog/%s/songs/%s", storefront, adamId), nil) 1281 | if err != nil { 1282 | return nil, err 1283 | } 1284 | query := url.Values{} 1285 | query.Set("extend", "extendedAssetUrls") 1286 | query.Set("include", "albums") 1287 | request.URL.RawQuery = query.Encode() 1288 | 1289 | request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", token)) 1290 | 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)") 1291 | request.Header.Set("Origin", "https://music.apple.com") 1292 | 1293 | do, err := http.DefaultClient.Do(request) 1294 | if err != nil { 1295 | return nil, err 1296 | } 1297 | defer do.Body.Close() 1298 | if do.StatusCode != http.StatusOK { 1299 | return nil, errors.New(do.Status) 1300 | } 1301 | 1302 | obj := new(ApiResult) 1303 | err = json.NewDecoder(do.Body).Decode(&obj) 1304 | if err != nil { 1305 | return nil, err 1306 | } 1307 | 1308 | for _, d := range obj.Data { 1309 | if d.ID == adamId { 1310 | return &d, nil 1311 | } 1312 | } 1313 | return nil, nil 1314 | } 1315 | 1316 | func getToken() (string, error) { 1317 | req, err := http.NewRequest("GET", "https://beta.music.apple.com", nil) 1318 | if err != nil { 1319 | return "", err 1320 | } 1321 | 1322 | resp, err := http.DefaultClient.Do(req) 1323 | if err != nil { 1324 | return "", err 1325 | } 1326 | defer resp.Body.Close() 1327 | 1328 | body, err := io.ReadAll(resp.Body) 1329 | if err != nil { 1330 | return "", err 1331 | } 1332 | 1333 | regex := regexp.MustCompile(`/assets/index-legacy-[^/]+\.js`) 1334 | indexJsUri := regex.FindString(string(body)) 1335 | 1336 | req, err = http.NewRequest("GET", "https://beta.music.apple.com"+indexJsUri, nil) 1337 | if err != nil { 1338 | return "", err 1339 | } 1340 | 1341 | resp, err = http.DefaultClient.Do(req) 1342 | if err != nil { 1343 | return "", err 1344 | } 1345 | defer resp.Body.Close() 1346 | 1347 | body, err = io.ReadAll(resp.Body) 1348 | if err != nil { 1349 | return "", err 1350 | } 1351 | 1352 | regex = regexp.MustCompile(`eyJh([^"]*)`) 1353 | token := regex.FindString(string(body)) 1354 | 1355 | return token, nil 1356 | } 1357 | 1358 | type ApiResult struct { 1359 | Data []SongData `json:"data"` 1360 | } 1361 | 1362 | type SongAttributes struct { 1363 | ArtistName string `json:"artistName"` 1364 | DiscNumber int `json:"discNumber"` 1365 | GenreNames []string `json:"genreNames"` 1366 | ExtendedAssetUrls struct { 1367 | EnhancedHls string `json:"enhancedHls"` 1368 | } `json:"extendedAssetUrls"` 1369 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1370 | ReleaseDate string `json:"releaseDate"` 1371 | Name string `json:"name"` 1372 | Isrc string `json:"isrc"` 1373 | AlbumName string `json:"albumName"` 1374 | TrackNumber int `json:"trackNumber"` 1375 | ComposerName string `json:"composerName"` 1376 | } 1377 | 1378 | type AlbumAttributes struct { 1379 | ArtistName string `json:"artistName"` 1380 | IsSingle bool `json:"isSingle"` 1381 | IsComplete bool `json:"isComplete"` 1382 | GenreNames []string `json:"genreNames"` 1383 | TrackCount int `json:"trackCount"` 1384 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1385 | ReleaseDate string `json:"releaseDate"` 1386 | Name string `json:"name"` 1387 | RecordLabel string `json:"recordLabel"` 1388 | Upc string `json:"upc"` 1389 | Copyright string `json:"copyright"` 1390 | IsCompilation bool `json:"isCompilation"` 1391 | } 1392 | 1393 | type SongData struct { 1394 | ID string `json:"id"` 1395 | Attributes SongAttributes `json:"attributes"` 1396 | Relationships struct { 1397 | Albums struct { 1398 | Data []struct { 1399 | ID string `json:"id"` 1400 | Type string `json:"type"` 1401 | Href string `json:"href"` 1402 | Attributes AlbumAttributes `json:"attributes"` 1403 | } `json:"data"` 1404 | } `json:"albums"` 1405 | Artists struct { 1406 | Href string `json:"href"` 1407 | Data []struct { 1408 | ID string `json:"id"` 1409 | Type string `json:"type"` 1410 | Href string `json:"href"` 1411 | } `json:"data"` 1412 | } `json:"artists"` 1413 | } `json:"relationships"` 1414 | } 1415 | 1416 | type SongResult struct { 1417 | Artwork struct { 1418 | Width int `json:"width"` 1419 | URL string `json:"url"` 1420 | Height int `json:"height"` 1421 | TextColor3 string `json:"textColor3"` 1422 | TextColor2 string `json:"textColor2"` 1423 | TextColor4 string `json:"textColor4"` 1424 | HasAlpha bool `json:"hasAlpha"` 1425 | TextColor1 string `json:"textColor1"` 1426 | BgColor string `json:"bgColor"` 1427 | HasP3 bool `json:"hasP3"` 1428 | SupportsLayeredImage bool `json:"supportsLayeredImage"` 1429 | } `json:"artwork"` 1430 | ArtistName string `json:"artistName"` 1431 | CollectionID string `json:"collectionId"` 1432 | DiscNumber int `json:"discNumber"` 1433 | GenreNames []string `json:"genreNames"` 1434 | ID string `json:"id"` 1435 | DurationInMillis int `json:"durationInMillis"` 1436 | ReleaseDate string `json:"releaseDate"` 1437 | ContentRatingsBySystem struct { 1438 | } `json:"contentRatingsBySystem"` 1439 | Name string `json:"name"` 1440 | Composer struct { 1441 | Name string `json:"name"` 1442 | URL string `json:"url"` 1443 | } `json:"composer"` 1444 | EditorialArtwork struct { 1445 | } `json:"editorialArtwork"` 1446 | CollectionName string `json:"collectionName"` 1447 | AssetUrls struct { 1448 | Plus string `json:"plus"` 1449 | Lightweight string `json:"lightweight"` 1450 | SuperLightweight string `json:"superLightweight"` 1451 | LightweightPlus string `json:"lightweightPlus"` 1452 | EnhancedHls string `json:"enhancedHls"` 1453 | } `json:"assetUrls"` 1454 | AudioTraits []string `json:"audioTraits"` 1455 | Kind string `json:"kind"` 1456 | Copyright string `json:"copyright"` 1457 | ArtistID string `json:"artistId"` 1458 | Genres []struct { 1459 | GenreID string `json:"genreId"` 1460 | Name string `json:"name"` 1461 | URL string `json:"url"` 1462 | MediaType string `json:"mediaType"` 1463 | } `json:"genres"` 1464 | TrackNumber int `json:"trackNumber"` 1465 | AudioLocale string `json:"audioLocale"` 1466 | Offers []struct { 1467 | ActionText struct { 1468 | Short string `json:"short"` 1469 | Medium string `json:"medium"` 1470 | Long string `json:"long"` 1471 | Downloaded string `json:"downloaded"` 1472 | Downloading string `json:"downloading"` 1473 | } `json:"actionText"` 1474 | Type string `json:"type"` 1475 | PriceFormatted string `json:"priceFormatted"` 1476 | Price float64 `json:"price"` 1477 | BuyParams string `json:"buyParams"` 1478 | Variant string `json:"variant,omitempty"` 1479 | Assets []struct { 1480 | Flavor string `json:"flavor"` 1481 | Preview struct { 1482 | Duration int `json:"duration"` 1483 | URL string `json:"url"` 1484 | } `json:"preview"` 1485 | Size int `json:"size"` 1486 | Duration int `json:"duration"` 1487 | } `json:"assets"` 1488 | } `json:"offers"` 1489 | } 1490 | type iTunesLookup struct { 1491 | Results map[string]SongResult `json:"results"` 1492 | } 1493 | 1494 | type Meta struct { 1495 | Context string `json:"@context"` 1496 | Type string `json:"@type"` 1497 | Name string `json:"name"` 1498 | Description string `json:"description"` 1499 | Tracks []struct { 1500 | Type string `json:"@type"` 1501 | Name string `json:"name"` 1502 | Audio struct { 1503 | Type string `json:"@type"` 1504 | } `json:"audio"` 1505 | Offers struct { 1506 | Type string `json:"@type"` 1507 | Category string `json:"category"` 1508 | Price int `json:"price"` 1509 | } `json:"offers"` 1510 | Duration string `json:"duration"` 1511 | } `json:"tracks"` 1512 | Citation []interface{} `json:"citation"` 1513 | WorkExample []struct { 1514 | Type string `json:"@type"` 1515 | Name string `json:"name"` 1516 | URL string `json:"url"` 1517 | Audio struct { 1518 | Type string `json:"@type"` 1519 | } `json:"audio"` 1520 | Offers struct { 1521 | Type string `json:"@type"` 1522 | Category string `json:"category"` 1523 | Price int `json:"price"` 1524 | } `json:"offers"` 1525 | Duration string `json:"duration"` 1526 | } `json:"workExample"` 1527 | Genre []string `json:"genre"` 1528 | DatePublished time.Time `json:"datePublished"` 1529 | ByArtist struct { 1530 | Type string `json:"@type"` 1531 | URL string `json:"url"` 1532 | Name string `json:"name"` 1533 | } `json:"byArtist"` 1534 | } 1535 | 1536 | type AutoGenerated struct { 1537 | Data []struct { 1538 | ID string `json:"id"` 1539 | Type string `json:"type"` 1540 | Href string `json:"href"` 1541 | Attributes struct { 1542 | Artwork struct { 1543 | Width int `json:"width"` 1544 | Height int `json:"height"` 1545 | URL string `json:"url"` 1546 | BgColor string `json:"bgColor"` 1547 | TextColor1 string `json:"textColor1"` 1548 | TextColor2 string `json:"textColor2"` 1549 | TextColor3 string `json:"textColor3"` 1550 | TextColor4 string `json:"textColor4"` 1551 | } `json:"artwork"` 1552 | ArtistName string `json:"artistName"` 1553 | IsSingle bool `json:"isSingle"` 1554 | URL string `json:"url"` 1555 | IsComplete bool `json:"isComplete"` 1556 | GenreNames []string `json:"genreNames"` 1557 | TrackCount int `json:"trackCount"` 1558 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1559 | ReleaseDate string `json:"releaseDate"` 1560 | Name string `json:"name"` 1561 | RecordLabel string `json:"recordLabel"` 1562 | Upc string `json:"upc"` 1563 | AudioTraits []string `json:"audioTraits"` 1564 | Copyright string `json:"copyright"` 1565 | PlayParams struct { 1566 | ID string `json:"id"` 1567 | Kind string `json:"kind"` 1568 | } `json:"playParams"` 1569 | IsCompilation bool `json:"isCompilation"` 1570 | } `json:"attributes"` 1571 | Relationships struct { 1572 | RecordLabels struct { 1573 | Href string `json:"href"` 1574 | Data []interface{} `json:"data"` 1575 | } `json:"record-labels"` 1576 | Artists struct { 1577 | Href string `json:"href"` 1578 | Data []struct { 1579 | ID string `json:"id"` 1580 | Type string `json:"type"` 1581 | Href string `json:"href"` 1582 | Attributes struct { 1583 | Name string `json:"name"` 1584 | } `json:"attributes"` 1585 | } `json:"data"` 1586 | } `json:"artists"` 1587 | Tracks struct { 1588 | Href string `json:"href"` 1589 | Data []struct { 1590 | ID string `json:"id"` 1591 | Type string `json:"type"` 1592 | Href string `json:"href"` 1593 | Attributes struct { 1594 | Previews []struct { 1595 | URL string `json:"url"` 1596 | } `json:"previews"` 1597 | Artwork struct { 1598 | Width int `json:"width"` 1599 | Height int `json:"height"` 1600 | URL string `json:"url"` 1601 | BgColor string `json:"bgColor"` 1602 | TextColor1 string `json:"textColor1"` 1603 | TextColor2 string `json:"textColor2"` 1604 | TextColor3 string `json:"textColor3"` 1605 | TextColor4 string `json:"textColor4"` 1606 | } `json:"artwork"` 1607 | ArtistName string `json:"artistName"` 1608 | URL string `json:"url"` 1609 | DiscNumber int `json:"discNumber"` 1610 | GenreNames []string `json:"genreNames"` 1611 | HasTimeSyncedLyrics bool `json:"hasTimeSyncedLyrics"` 1612 | IsMasteredForItunes bool `json:"isMasteredForItunes"` 1613 | DurationInMillis int `json:"durationInMillis"` 1614 | ReleaseDate string `json:"releaseDate"` 1615 | Name string `json:"name"` 1616 | Isrc string `json:"isrc"` 1617 | AudioTraits []string `json:"audioTraits"` 1618 | HasLyrics bool `json:"hasLyrics"` 1619 | AlbumName string `json:"albumName"` 1620 | PlayParams struct { 1621 | ID string `json:"id"` 1622 | Kind string `json:"kind"` 1623 | } `json:"playParams"` 1624 | TrackNumber int `json:"trackNumber"` 1625 | AudioLocale string `json:"audioLocale"` 1626 | ComposerName string `json:"composerName"` 1627 | } `json:"attributes"` 1628 | Relationships struct { 1629 | Artists struct { 1630 | Href string `json:"href"` 1631 | Data []struct { 1632 | ID string `json:"id"` 1633 | Type string `json:"type"` 1634 | Href string `json:"href"` 1635 | Attributes struct { 1636 | Name string `json:"name"` 1637 | } `json:"attributes"` 1638 | } `json:"data"` 1639 | } `json:"artists"` 1640 | } `json:"relationships"` 1641 | } `json:"data"` 1642 | } `json:"tracks"` 1643 | } `json:"relationships"` 1644 | } `json:"data"` 1645 | } 1646 | --------------------------------------------------------------------------------