From 3726ac4c8adb31f2ce93a103fbb218dc54a9d506 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Sun, 9 Feb 2025 09:36:51 +0800 Subject: [PATCH 1/5] dev: MV dl (need n-m3u8dl-re and mp4decrypt) --- main.go | 100 ++++++++++++++++++++++++++++++++++++++++- utils/runv3/key/key.go | 4 +- utils/runv3/runv3.go | 43 ++++++++++++------ 3 files changed, 131 insertions(+), 16 deletions(-) diff --git a/main.go b/main.go index d28f757..9a75e78 100644 --- a/main.go +++ b/main.go @@ -100,6 +100,16 @@ func checkUrl(url string) (string, string) { return matches[0][1], matches[0][2] } } +func checkUrlMv(url string) (string) { + pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/music-video|\/music-video\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) + matches := pat.FindAllStringSubmatch(url, -1) + + if matches == nil { + return "" + } else { + return matches[0][2] + } +} func checkUrlSong(url string) (string, string) { pat := regexp.MustCompile(`^(?:https:\/\/(?:beta\.music|music)\.apple\.com\/(\w{2})(?:\/song|\/song\/.+))\/(?:id)?(\d[^\D]+)(?:$|\?)`) matches := pat.FindAllStringSubmatch(url, -1) @@ -583,7 +593,7 @@ func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, tr counter.Error++ return } - err := runv3.Run(track.ID, trackPath, token, mediaUserToken) + _, err := runv3.Run(track.ID, trackPath, token, mediaUserToken, false) if err != nil { fmt.Println("Failed to dl aac-lc:", err) counter.Error++ @@ -1093,6 +1103,11 @@ func main() { return } os.Args = args + //mv dl dev + if strings.Contains(os.Args[0], "/music-video/") { + _ = mvDownloader(checkUrlMv(os.Args[0]), Config.AlacSaveFolder, token, Config.MediaUserToken) + return + } if strings.Contains(os.Args[0], "/artist/") { urlArtistName, err := getUrlArtistName(os.Args[0], token) if err != nil { @@ -1153,6 +1168,89 @@ func main() { } } +func mvDownloader(adamID string, saveDir string, token string, mediaUserToken string)(error){ + vidPath := filepath.Join(saveDir, fmt.Sprintf("%s.mp4", adamID)) + audPath := filepath.Join(saveDir, fmt.Sprintf("%s.m4a", adamID)) + mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mkv", adamID)) + exists, _ := fileExists(mvOutPath) + if exists { + fmt.Println("MV already exists locally.") + return nil + } + mvm3u8url, _, _ := runv3.GetWebplayback(adamID, token, mediaUserToken, true) + //videom3u8url, audiom3u8url , _ := mvQualitySelect(mvm3u8url) + videom3u8url, _ := extractVideo(mvm3u8url) + audiom3u8url, _ := extractMvAudio(mvm3u8url) + //fmt.Println(videom3u8url) + //fmt.Println(audiom3u8url) + videokey, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) + audiokey, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true) + //fmt.Println(videokey) + //fmt.Println(audiokey) + cmd1 := exec.Command("n-m3u8dl-re", videom3u8url, "--key", videokey, "--decryption-engine", "MP4DECRYPT", "--save-dir", saveDir, "--save-name", adamID) + cmd2 := exec.Command("n-m3u8dl-re", audiom3u8url, "--key", audiokey, "--decryption-engine", "MP4DECRYPT", "--save-dir", saveDir, "--save-name", adamID) + cmd3 := exec.Command("MP4Box", "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath) + fmt.Printf("MVvid Downloading...") + if err := cmd1.Run(); err != nil { + fmt.Printf("MVvid Download failed: %v\n", err) + return err + } + fmt.Printf("\rMVvid Downloaded. \n") + fmt.Printf("MVaud Downloading...") + if err := cmd2.Run(); err != nil { + fmt.Printf("MVaud Download failed: %v\n", err) + return err + } + fmt.Printf("\rMVaud Downloaded. \n") + fmt.Printf("MV Remuxing...") + if err := cmd3.Run(); err != nil { + fmt.Printf("MV mux failed: %v\n", err) + return err + } + fmt.Printf("\rMV Remuxed. \n") + _ = os.Remove(vidPath) + _ = os.Remove(audPath) + return nil +} + +func extractMvAudio(c string) (string, error) { + MediaUrl, err := url.Parse(c) + if err != nil { + return "", err + } + resp, err := http.Get(c) + if err != nil { + return "", err + } + defer resp.Body.Close() + if resp.StatusCode != http.StatusOK { + return "", errors.New(resp.Status) + } + body, err := io.ReadAll(resp.Body) + if err != nil { + return "", err + } + videoString := string(body) + from, listType, err := m3u8.DecodeFrom(strings.NewReader(videoString), true) + if err != nil || listType != m3u8.MASTER { + return "", errors.New("m3u8 not of media type") + } + video := from.(*m3u8.MasterPlaylist) + var streamUrl *url.URL + for _, variant := range video.Variants { + for _, audiov := range variant.Alternatives { + if audiov.GroupId == "audio-stereo-256" { + streamUrl, _ = MediaUrl.Parse(audiov.URI) + break + } + } + } + if streamUrl == nil { + return "", errors.New("no video codec found") + } + return streamUrl.String(), nil +} + func conventSyllableTTMLToLRC(ttml string) (string, error) { parsedTTML := etree.NewDocument() err := parsedTTML.ReadFromString(ttml) diff --git a/utils/runv3/key/key.go b/utils/runv3/key/key.go index 6a4731f..166fa70 100644 --- a/utils/runv3/key/key.go +++ b/utils/runv3/key/key.go @@ -64,8 +64,8 @@ func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string, for _, key := range keys { if key.Type == wv.License_KeyContainer_CONTENT { - // command += "--key " + hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value) - command += hex.EncodeToString(key.Value) + command += hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value) + //command += hex.EncodeToString(key.Value) keybt = key.Value } } diff --git a/utils/runv3/runv3.go b/utils/runv3/runv3.go index 9b8d147..0f3cba9 100644 --- a/utils/runv3/runv3.go +++ b/utils/runv3/runv3.go @@ -106,7 +106,7 @@ func AfterRequest(Response *requests.Response) ([]byte, error) { } return License, nil } -func getWebplayback(adamId string, authtoken string, mutoken string) (string, string, error) { +func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool) (string, string, error) { url := "https://play.music.apple.com/WebObjects/MZPlay.woa/wa/webPlayback" postData := map[string]string{ "salableAdamId": adamId, @@ -144,6 +144,9 @@ func getWebplayback(adamId string, authtoken string, mutoken string) (string, st fmt.Println("json err:", err) return "", "", err } + if mvmode { + return obj.List[0].HlsPlaylistUrl, "", nil + } if len(obj.List) > 0 { // 遍历 Assets for i, _ := range obj.List[0].Assets { @@ -162,6 +165,7 @@ func getWebplayback(adamId string, authtoken string, mutoken string) (string, st type Songlist struct { List []struct { Hlsurl string `json:"hls-key-cert-url"` + HlsPlaylistUrl string `json:"hls-playlist-url"` Assets []struct { Flavor string `json:"flavor"` URL string `json:"URL"` @@ -235,11 +239,21 @@ func extsong(b string)(bytes.Buffer){ io.Copy(io.MultiWriter(&buffer, bar), resp.Body) return buffer } -func Run(adamId string, trackpath string, authtoken string, mutoken string)(error) { - - fileurl, kidBase64, err := getWebplayback(adamId, authtoken, mutoken) - if err != nil { - return err +func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmode bool)(string, error) { + var keystr string //for mv key + var fileurl string + var kidBase64 string + var err error + if mvmode { + kidBase64, _, err = extractKidBase64(trackpath) + if err != nil { + return "", err + } + } else { + fileurl, kidBase64, err = GetWebplayback(adamId, authtoken, mutoken, false) + if err != nil { + return "", err + } } ctx := context.Background() ctx = context.WithValue(ctx, "pssh", kidBase64) @@ -248,7 +262,7 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string)(erro //fmt.Println(pssh) if err != nil { fmt.Println(err) - return err + return "", err } headers := map[string]interface{}{ "authorization": "Bearer " + authtoken, @@ -263,10 +277,13 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string)(erro AfterRequest: AfterRequest, } key.CdmInit() - _, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil) + keystr, keybt, err := key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/wa/acquireWebPlaybackLicense", pssh, nil) if err != nil { fmt.Println(err) - return err + return "", err + } + if mvmode { + return keystr, nil } body := extsong(fileurl) fmt.Print("Downloaded\n") @@ -276,7 +293,7 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string)(erro err = DecryptMP4(&body, keybt, &buffer) if err != nil { fmt.Print("Decryption failed\n") - return err + return "", err } else { fmt.Print("Decrypted\n") } @@ -284,16 +301,16 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string)(erro ofh, err := os.Create(trackpath) if err != nil { fmt.Printf("创建文件失败: %v\n", err) - return err + return "", err } defer ofh.Close() _, err = ofh.Write(buffer.Bytes()) if err != nil { fmt.Printf("写入文件失败: %v\n", err) - return err + return "", err } - return nil + return "", nil } // DecryptMP4Auto decrypts a fragmented MP4 file with the set of keys retreived from the widevice license // by automatically selecting the appropriate key. Supports CENC and CBCS schemes. From 0f692422c0497b4cf8967068fd0e8a0b5d80fc98 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Tue, 11 Feb 2025 05:27:55 +0800 Subject: [PATCH 2/5] dev: MV dl (need mp4decrypt) --- main.go | 44 ++++++++---------- utils/runv3/key/key.go | 4 +- utils/runv3/runv3.go | 100 +++++++++++++++++++++++++++++++---------- 3 files changed, 97 insertions(+), 51 deletions(-) diff --git a/main.go b/main.go index 9a75e78..9d46b51 100644 --- a/main.go +++ b/main.go @@ -400,7 +400,7 @@ func writeCover(sanAlbumFolder, name string, url string) error { return err } if exists { - return nil + _ = os.Remove(covPath) } if Config.CoverFormat == "png" { re := regexp.MustCompile(`\{w\}x\{h\}`) @@ -970,7 +970,7 @@ func rip(albumId string, token string, storefront string, mediaUserToken string, continue } if isInArray(selected, trackNum) { - counter.Total++ + //counter.Total++ downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter) } } @@ -1167,10 +1167,9 @@ func main() { counter = structs.Counter{} } } - func mvDownloader(adamID string, saveDir string, token string, mediaUserToken string)(error){ - vidPath := filepath.Join(saveDir, fmt.Sprintf("%s.mp4", adamID)) - audPath := filepath.Join(saveDir, fmt.Sprintf("%s.m4a", adamID)) + vidPath := filepath.Join(saveDir, fmt.Sprintf("%s_vid.mp4", adamID)) + audPath := filepath.Join(saveDir, fmt.Sprintf("%s_aud.mp4", adamID)) mvOutPath := filepath.Join(saveDir, fmt.Sprintf("%s.mkv", adamID)) exists, _ := fileExists(mvOutPath) if exists { @@ -1183,33 +1182,28 @@ func mvDownloader(adamID string, saveDir string, token string, mediaUserToken st audiom3u8url, _ := extractMvAudio(mvm3u8url) //fmt.Println(videom3u8url) //fmt.Println(audiom3u8url) - videokey, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) - audiokey, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true) - //fmt.Println(videokey) - //fmt.Println(audiokey) - cmd1 := exec.Command("n-m3u8dl-re", videom3u8url, "--key", videokey, "--decryption-engine", "MP4DECRYPT", "--save-dir", saveDir, "--save-name", adamID) - cmd2 := exec.Command("n-m3u8dl-re", audiom3u8url, "--key", audiokey, "--decryption-engine", "MP4DECRYPT", "--save-dir", saveDir, "--save-name", adamID) - cmd3 := exec.Command("MP4Box", "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath) - fmt.Printf("MVvid Downloading...") - if err := cmd1.Run(); err != nil { - fmt.Printf("MVvid Download failed: %v\n", err) - return err - } - fmt.Printf("\rMVvid Downloaded. \n") - fmt.Printf("MVaud Downloading...") - if err := cmd2.Run(); err != nil { - fmt.Printf("MVaud Download failed: %v\n", err) - return err - } - fmt.Printf("\rMVaud Downloaded. \n") + + videokeyAndUrls, _ := runv3.Run(adamID, videom3u8url, token, mediaUserToken, true) + audiokeyAndUrls, _ := runv3.Run(adamID, audiom3u8url, token, mediaUserToken, true) + //fmt.Println(videokeyAndUrls) + //fmt.Println(audiokeyAndUrls) + + fmt.Println("MV-VIDEO") + _ = runv3.ExtMvData(videokeyAndUrls, vidPath) + + fmt.Println("MV-AUDIO") + _ = runv3.ExtMvData(audiokeyAndUrls, audPath) + + muxCmd := exec.Command("MP4Box", "-quiet", "-add", vidPath, "-add", audPath, "-keep-utc", "-new", mvOutPath) fmt.Printf("MV Remuxing...") - if err := cmd3.Run(); err != nil { + if err := muxCmd.Run(); err != nil { fmt.Printf("MV mux failed: %v\n", err) return err } fmt.Printf("\rMV Remuxed. \n") _ = os.Remove(vidPath) _ = os.Remove(audPath) + return nil } diff --git a/utils/runv3/key/key.go b/utils/runv3/key/key.go index 166fa70..449cd1f 100644 --- a/utils/runv3/key/key.go +++ b/utils/runv3/key/key.go @@ -64,8 +64,8 @@ func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string, for _, key := range keys { if key.Type == wv.License_KeyContainer_CONTENT { - command += hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value) - //command += hex.EncodeToString(key.Value) + //command += hex.EncodeToString(key.ID) + ":" + hex.EncodeToString(key.Value) + command += hex.EncodeToString(key.Value) keybt = key.Value } } diff --git a/utils/runv3/runv3.go b/utils/runv3/runv3.go index 0f3cba9..310939e 100644 --- a/utils/runv3/runv3.go +++ b/utils/runv3/runv3.go @@ -22,6 +22,7 @@ import ( "github.com/grafov/m3u8" "strings" "github.com/schollz/progressbar/v3" + "os/exec" ) type PlaybackLicense struct { @@ -151,7 +152,7 @@ func GetWebplayback(adamId string, authtoken string, mutoken string, mvmode bool // 遍历 Assets for i, _ := range obj.List[0].Assets { if obj.List[0].Assets[i].Flavor == "28:ctrp256" { - kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL) + kidBase64, fileurl, err := extractKidBase64(obj.List[0].Assets[i].URL, false) if err != nil { return "", "", err } @@ -174,7 +175,7 @@ type Songlist struct { Status int `json:"status"` } -func extractKidBase64(b string) (string, string, error) { +func extractKidBase64(b string, mvmode bool) (string, string, error) { resp, err := http.Get(b) if err != nil { return "", "", err @@ -193,7 +194,7 @@ func extractKidBase64(b string) (string, string, error) { return "", "", err } var kidbase64 string - var fileurl string + var urlBuilder strings.Builder if listType == m3u8.MEDIA { mediaPlaylist := from.(*m3u8.MediaPlaylist) if mediaPlaylist.Key != nil { @@ -201,15 +202,30 @@ func extractKidBase64(b string) (string, string, error) { kidbase64 = split[1] lastSlashIndex := strings.LastIndex(b, "/") // 截取最后一个斜杠之前的部分 - fileurl = b[:lastSlashIndex] + "/" + mediaPlaylist.Map.URI + urlBuilder.WriteString(b[:lastSlashIndex]) + urlBuilder.WriteString("/") + urlBuilder.WriteString(mediaPlaylist.Map.URI) + //fileurl = b[:lastSlashIndex] + "/" + mediaPlaylist.Map.URI //fmt.Println("Extracted URI:", mediaPlaylist.Map.URI) + if mvmode { + for _, segment := range mediaPlaylist.Segments { + if segment != nil { + fmt.Println("Extracted URI:", segment.URI) + urlBuilder.WriteString(";") + urlBuilder.WriteString(b[:lastSlashIndex]) + urlBuilder.WriteString("/") + urlBuilder.WriteString(segment.URI) + //fileurl = fileurl + ";" + b[:lastSlashIndex] + "/" + segment.URI + } + } + } } else { fmt.Println("No key information found") } } else { fmt.Println("Not a media playlist") } - return kidbase64, fileurl, nil + return kidbase64, urlBuilder.String(), nil } func extsong(b string)(bytes.Buffer){ resp, err := http.Get(b) @@ -245,7 +261,7 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo var kidBase64 string var err error if mvmode { - kidBase64, _, err = extractKidBase64(trackpath) + kidBase64, fileurl, err = extractKidBase64(trackpath, true) if err != nil { return "", err } @@ -283,7 +299,8 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo return "", err } if mvmode { - return keystr, nil + keyAndUrls := "1:" + keystr + ";" + fileurl + return keyAndUrls, nil } body := extsong(fileurl) fmt.Print("Downloaded\n") @@ -312,23 +329,58 @@ func Run(adamId string, trackpath string, authtoken string, mutoken string, mvmo } return "", nil } -// DecryptMP4Auto decrypts a fragmented MP4 file with the set of keys retreived from the widevice license -// by automatically selecting the appropriate key. Supports CENC and CBCS schemes. -// func DecryptMP4Auto(r io.Reader, keys []*Key, w io.Writer) error { -// // Extract content key -// var key []byte -// for _, k := range keys { -// if k.Type == wvpb.License_KeyContainer_CONTENT { -// key = k.Key -// break -// } -// } -// if key == nil { -// return fmt.Errorf("no %s key type found in the provided key set", wvpb.License_KeyContainer_CONTENT) -// } -// // Execute decryption -// return DecryptMP4(r, key, w) -// } + +func ExtMvData (keyAndUrls string, savePath string)(error) { + segments := strings.Split(keyAndUrls, ";") + key := segments[0] + //fmt.Println(key) + urls := segments[1:] + tempFile, err := os.CreateTemp("", "enc_mv_data-*.mp4") + if err != nil { + fmt.Printf("创建文件失败:%v\n", err) + return err + } + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + // 依次下载每个链接并写入文件 + bar := progressbar.DefaultBytes( + -1, + "Downloading...", + ) + barWriter := io.MultiWriter(tempFile, bar) + for _, url := range urls { + resp, err := http.Get(url) + if err != nil { + fmt.Printf("下载链接 %s 失败:%v\n", url, err) + return err + } + + // 将响应体写入输出文件 + _, err = io.Copy(barWriter, resp.Body) + defer resp.Body.Close() // 注意及时关闭响应体,避免资源泄露 + if err != nil { + fmt.Printf("写入文件失败:%v\n", err) + return err + } + + //fmt.Printf("第 %d 个链接 %s 下载并写入完成\n", idx+1, url) + } + tempFile.Close() + fmt.Println("\nDownloaded.") + + cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), savePath) + //outlog, err := cmd1.CombinedOutput() + if err := cmd1.Run(); err != nil { + fmt.Printf("Decrypt failed: %v\n", err) + //fmt.Printf("Output:\n%s\n", outlog) + return err + } else { + fmt.Println("Decrypted.") + } + return nil +} + // DecryptMP4 decrypts a fragmented MP4 file with keys from widevice license. Supports CENC and CBCS schemes. func DecryptMP4(r io.Reader, key []byte, w io.Writer) error { From 95ebc25c96c93c80fe08844c7207cc00bc54f478 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Tue, 11 Feb 2025 05:47:44 +0800 Subject: [PATCH 3/5] fix: MV dir not found --- main.go | 1 + utils/runv3/runv3.go | 8 ++++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/main.go b/main.go index 9d46b51..df88fa7 100644 --- a/main.go +++ b/main.go @@ -1188,6 +1188,7 @@ func mvDownloader(adamID string, saveDir string, token string, mediaUserToken st //fmt.Println(videokeyAndUrls) //fmt.Println(audiokeyAndUrls) + os.MkdirAll(saveDir, os.ModePerm) fmt.Println("MV-VIDEO") _ = runv3.ExtMvData(videokeyAndUrls, vidPath) diff --git a/utils/runv3/runv3.go b/utils/runv3/runv3.go index 310939e..5247aad 100644 --- a/utils/runv3/runv3.go +++ b/utils/runv3/runv3.go @@ -210,7 +210,7 @@ func extractKidBase64(b string, mvmode bool) (string, string, error) { if mvmode { for _, segment := range mediaPlaylist.Segments { if segment != nil { - fmt.Println("Extracted URI:", segment.URI) + //fmt.Println("Extracted URI:", segment.URI) urlBuilder.WriteString(";") urlBuilder.WriteString(b[:lastSlashIndex]) urlBuilder.WriteString("/") @@ -370,10 +370,10 @@ func ExtMvData (keyAndUrls string, savePath string)(error) { fmt.Println("\nDownloaded.") cmd1 := exec.Command("mp4decrypt", "--key", key, tempFile.Name(), savePath) - //outlog, err := cmd1.CombinedOutput() - if err := cmd1.Run(); err != nil { + outlog, err := cmd1.CombinedOutput() + if err != nil { fmt.Printf("Decrypt failed: %v\n", err) - //fmt.Printf("Output:\n%s\n", outlog) + fmt.Printf("Output:\n%s\n", outlog) return err } else { fmt.Println("Decrypted.") From 61dfea68e1e52b6115cbcc8a251af7c8cf5e064f Mon Sep 17 00:00:00 2001 From: itouakirai Date: Tue, 11 Feb 2025 08:26:04 +0800 Subject: [PATCH 4/5] add: MV dl(need mp4decrypt) --- README.md | 55 +++++++++++++++---------------------------------------- main.go | 53 ++++++++++++++++++++++++++++++++++++++++++++++++----- 2 files changed, 63 insertions(+), 45 deletions(-) diff --git a/README.md b/README.md index 3525ad9..2029cf9 100644 --- a/README.md +++ b/README.md @@ -2,23 +2,15 @@ ### 添加功能 -1. 调用外部MP4Box添加tag -2. 更改目录结构为 歌手名\专辑名 ;Atmos下载文件则另外移动到AM-DL-Atmos downloads,并更改目录结构为 歌手名\专辑名 [Atmos] -3. 运行结束后显示总体完成情况 -4. 自动内嵌封面和LRC歌词(需要media-user-token,获取方式看最后的说明) -5. 自动构建 可以到 [Actions](https://github.com/zhaarey/apple-music-alac-atmos-downloader/actions) 页面下载最新自动构建版本 可以直接`main.exe url` -6. 支持逐词与未同步歌词 -7. 新增get-m3u8-from-device 改为true 且设置端口`adb forward tcp:20020 tcp:20020`即从模拟器获取m3u8 -8. 文件夹和文件支持模板 -9. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` 自动选择歌手的所有专辑 -10. 新增[wrapper](https://github.com/zhaarey/wrapper/releases)模式 目前只能linux运行,解密速度超快,基本秒解 -11. `limit-max`支持限制长度 默认200 -12. 现已支持arm64解密 -13. 下载解密部分更换为Sendy McSenderson的代码,实现边下载边解密 +1. 支持内嵌封面和LRC歌词(需要`media-user-token`,获取方式看最后的说明) +2. 支持获取逐词与未同步歌词 +3. 支持下载歌手 `go run main.go https://music.apple.com/us/artist/taylor-swift/159260351` `--all-album` 自动选择歌手的所有专辑 +4. 下载解密部分更换为Sendy McSenderson的代码,实现边下载边解密,解决大文件解密时内存不足 +5. MV下载,需要安装[mp4decrypt](https://www.bento4.com/downloads/) ### Special thanks to `chocomint` for creating `agent-arm64.js` -本项目暂不支持MV下载,对于下载aac-lc则必须填入有订阅的media-user-token +对于获取`aac-lc` `MV` `歌词` 必须填入有订阅的`media-user-token` - `alac (audio-alac-stereo)` - `ec3 (audio-atmos / audio-ec3)` @@ -26,38 +18,21 @@ - `aac-lc (audio-stereo)` - `aac-binaural (audio-stereo-binaural)` - `aac-downmix (audio-stereo-downmix)` - +- `MV` # Apple Music ALAC / Dolby Atmos Downloader Original script by Sorrow. Modified by me to include some fixes and improvements. ## How to use - -1. Create a virtual device on Android Studio with a image that doesn't have Google APIs. -2. Install Apple Music - - for x86 install this version of [Apple Music 3.6.0 beta4](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](https://f-droid.org/pt_BR/packages/com.aefyr.sai.fdroid/) to install it. - - for arm64 install the last version of [Apple Music](https://www.apkmirror.com/apk/apple/apple-music/). - -3. Launch Apple Music and sign in to your account. Subscription required. -4. Port forward 10020 TCP: `adb forward tcp:10020 tcp:10020`. -5. Start frida server. -6. Start the frida agent: - - for x86 `frida -U -l agent.js -f com.apple.android.music` - - for arm64 `frida -U -l agent-arm64.js -f com.apple.android.music` - - -7. Start downloading some albums: `go run main.go https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511`. -8. Start downloading single song: `go run main.go --song https://music.apple.com/us/album/never-gonna-give-you-up-2022-remaster/1624945511?i=1624945512` or `go run main.go https://music.apple.com/us/song/you-move-me-2022-remaster/1624945520`. -9. Start downloading select: `go run main.go --select https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511` input numbers separated by spaces. -10. Start downloading some playlists: `go run main.go https://music.apple.com/us/playlist/taylor-swift-essentials/pl.3950454ced8c45a3b0cc693c2a7db97b` or `go run main.go https://music.apple.com/us/playlist/hi-res-lossless-24-bit-192khz/pl.u-MDAWvpjt38370N`. -11. For dolby atmos: `go run main.go --atmos https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. -12. For aac: `go run main.go --aac https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. -13. For see quality: `go run main.go --debug https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. +1. Make sure the decryption program [wrapper](https://github.com/zhaarey/wrapper) is running +2. Start downloading some albums: `go run main.go https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511`. +3. Start downloading single song: `go run main.go --song https://music.apple.com/us/album/never-gonna-give-you-up-2022-remaster/1624945511?i=1624945512` or `go run main.go https://music.apple.com/us/song/you-move-me-2022-remaster/1624945520`. +4. Start downloading select: `go run main.go --select https://music.apple.com/us/album/whenever-you-need-somebody-2022-remaster/1624945511` input numbers separated by spaces. +5. Start downloading some playlists: `go run main.go https://music.apple.com/us/playlist/taylor-swift-essentials/pl.3950454ced8c45a3b0cc693c2a7db97b` or `go run main.go https://music.apple.com/us/playlist/hi-res-lossless-24-bit-192khz/pl.u-MDAWvpjt38370N`. +6. For dolby atmos: `go run main.go --atmos https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. +7. For aac: `go run main.go --aac https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. +8. For see quality: `go run main.go --debug https://music.apple.com/us/album/1989-taylors-version-deluxe/1713845538`. [中文教程-详见方法三](https://telegra.ph/Apple-Music-Alac高解析度无损音乐下载教程-04-02-2) diff --git a/main.go b/main.go index df88fa7..d846338 100644 --- a/main.go +++ b/main.go @@ -464,6 +464,29 @@ func contains(slice []string, item string) bool { func downloadTrack(trackNum int, trackTotal int, meta *structs.AutoGenerated, track structs.TrackData, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec string, counter *structs.Counter) { counter.Total++ fmt.Printf("Track %d of %d:\n", trackNum, trackTotal) + + //mv dl dev + if track.Type == "music-videos" { + if mediaUserToken == "" || len(mediaUserToken) <= 10 { + fmt.Println("meida-user-token is not set, skip MV dl") + counter.Success++ + return + } + if _, err := exec.LookPath("mp4decrypt"); err != nil { + fmt.Println("mp4decrypt is not found, skip MV dl") + counter.Success++ + return + } + err := mvDownloader(track.ID, sanAlbumFolder, token, mediaUserToken) + if err !=nil { + fmt.Println("\u26A0 Failed to dl MV:", err) + counter.Error++ + return + } + counter.Success++ + return + } + manifest, err := getInfoFromAdam(track.ID, token, storefront) if err != nil { fmt.Println("\u26A0 Failed to get manifest:", err) @@ -1103,11 +1126,6 @@ func main() { return } os.Args = args - //mv dl dev - if strings.Contains(os.Args[0], "/music-video/") { - _ = mvDownloader(checkUrlMv(os.Args[0]), Config.AlacSaveFolder, token, Config.MediaUserToken) - return - } if strings.Contains(os.Args[0], "/artist/") { urlArtistName, err := getUrlArtistName(os.Args[0], token) if err != nil { @@ -1130,6 +1148,31 @@ func main() { for albumNum, urlRaw := range os.Args { fmt.Printf("Album %d of %d:\n", albumNum+1, albumTotal) var storefront, albumId string + //mv dl dev + if strings.Contains(urlRaw, "/music-video/") { + if debug_mode { + continue + } + counter.Total++ + if Config.MediaUserToken == "" || len(Config.MediaUserToken) <= 10 { + fmt.Println("meida-user-token is not set, skip MV dl") + counter.Success++ + continue + } + if _, err := exec.LookPath("mp4decrypt"); err != nil { + fmt.Println("mp4decrypt is not found, skip MV dl") + counter.Success++ + continue + } + err := mvDownloader(checkUrlMv(urlRaw), Config.AlacSaveFolder, token, Config.MediaUserToken) + if err !=nil { + fmt.Println("\u26A0 Failed to dl MV:", err) + counter.Error++ + continue + } + counter.Success++ + continue + } if strings.Contains(urlRaw, "/song/") { urlRaw, err = getUrlSong(urlRaw, token) dl_song = true From 0a85fe785b5873b1212cd6bbdfc6a94f4d988398 Mon Sep 17 00:00:00 2001 From: itouakirai Date: Tue, 11 Feb 2025 09:29:08 +0800 Subject: [PATCH 5/5] fix: day tag --- go.mod | 2 +- go.sum | 4 ++-- main.go | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index 2613fbc..cc8f337 100644 --- a/go.mod +++ b/go.mod @@ -4,7 +4,6 @@ go 1.23.1 require ( github.com/Eyevinn/mp4ff v0.46.0 - github.com/Sorrow446/go-mp4tag v0.0.0-20240130220823-68ce31d53e37 github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 github.com/gospider007/requests v0.0.0-20250114011338-9562a203fa04 github.com/grafov/m3u8 v0.11.1 @@ -94,5 +93,6 @@ require ( require ( github.com/beevik/etree v1.3.0 + github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf gopkg.in/yaml.v2 v2.2.8 ) diff --git a/go.sum b/go.sum index ecf206a..47f17bd 100644 --- a/go.sum +++ b/go.sum @@ -23,8 +23,6 @@ github.com/PuerkitoBio/goquery v1.10.1 h1:Y8JGYUkXWTGRB6Ars3+j3kN0xg1YqqlwvdTV8W github.com/PuerkitoBio/goquery v1.10.1/go.mod h1:IYiHrOMps66ag56LEH7QYDDupKXyo5A8qrjIx3ZtujY= github.com/STARRY-S/zip v0.2.1 h1:pWBd4tuSGm3wtpoqRZZ2EAwOmcHK6XFf7bU9qcJXyFg= github.com/STARRY-S/zip v0.2.1/go.mod h1:xNvshLODWtC4EJ702g7cTYn13G53o1+X9BWnPFpcWV4= -github.com/Sorrow446/go-mp4tag v0.0.0-20240130220823-68ce31d53e37 h1:6X6U2D53ITfDGiyGN+sOVm/iFveFHrFRS7icGJ+u88M= -github.com/Sorrow446/go-mp4tag v0.0.0-20240130220823-68ce31d53e37/go.mod h1:l5rVvaRUrCot83416D6xggKCeFZQAXcv02tnJslG26s= github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg= github.com/andybalholm/brotli v1.1.1 h1:PR2pgnyFznKEugtsUo0xLdDop5SKXd5Qf5ysW+7XdTA= @@ -245,6 +243,8 @@ github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI= github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE= github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= +github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf h1:WzZoh9wvukQu2We8dw/bFmLfb5XsC5bGGU/Izhd/UOo= +github.com/zhaarey/go-mp4tag v0.0.0-20250210094042-22578afc09bf/go.mod h1:cqL6le//aG0AE1/VE1um2m+8dKa8te/WhHWqzrHMDys= go.mongodb.org/mongo-driver v1.17.2 h1:gvZyk8352qSfzyZ2UMWcpDpMSGEr1eqE4T793SqyhzM= go.mongodb.org/mongo-driver v1.17.2/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= diff --git a/main.go b/main.go index d846338..f50af9c 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( "strconv" "strings" - "github.com/Sorrow446/go-mp4tag" + "github.com/zhaarey/go-mp4tag" "github.com/spf13/pflag" "gopkg.in/yaml.v2" @@ -993,7 +993,6 @@ func rip(albumId string, token string, storefront string, mediaUserToken string, continue } if isInArray(selected, trackNum) { - //counter.Total++ downloadTrack(trackNum, trackTotal, meta, track, albumId, token, storefront, mediaUserToken, sanAlbumFolder, Codec, &counter) } }