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.