package runv3 import ( "context" "encoding/base64" "fmt" "path/filepath" "github.com/gospider007/requests" "google.golang.org/protobuf/proto" //"log/slog" cdm "main/utils/runv3/cdm" key "main/utils/runv3/key" "os" "bytes" "errors" "io" "github.com/Eyevinn/mp4ff/mp4" //"io/ioutil" "encoding/json" "net/http" "os/exec" "strings" "github.com/grafov/m3u8" "github.com/schollz/progressbar/v3" ) type PlaybackLicense struct { ErrorCode int `json:"errorCode"` License string `json:"license"` RenewAfter int `json:"renew-after"` Status int `json:"status"` } // func log() { // f, err := os.OpenFile("log.txt", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666) // if err != nil { // slog.Error("error opening file: %s", err) // } // defer func(f *os.File) { // err := f.Close() // if err != nil { // slog.Error("error closing file: %s", err) // } // }(f) // opts := slog.HandlerOptions{ // AddSource: true, // Level: slog.LevelDebug, // } // logger := slog.New(slog.NewJSONHandler(os.Stdout, &opts)) // slog.SetDefault(logger) // } func getPSSH(contentId string, kidBase64 string) (string, error) { kidBytes, err := base64.StdEncoding.DecodeString(kidBase64) if err != nil { return "", fmt.Errorf("failed to decode base64 KID: %v", err) } contentIdEncoded := base64.StdEncoding.EncodeToString([]byte(contentId)) algo := cdm.WidevineCencHeader_AESCTR widevineCencHeader := &cdm.WidevineCencHeader{ KeyId: [][]byte{kidBytes}, Algorithm: &algo, Provider: new(string), ContentId: []byte(contentIdEncoded), Policy: new(string), } widevineCenc, err := proto.Marshal(widevineCencHeader) if err != nil { return "", fmt.Errorf("failed to marshal WidevineCencHeader: %v", err) } //最前面添加32字节 widevineCenc = append([]byte("0123456789abcdef0123456789abcdef"), widevineCenc...) pssh := base64.StdEncoding.EncodeToString(widevineCenc) return pssh, nil } func BeforeRequest(cl *requests.Client, preCtx context.Context, method string, href string, options ...requests.RequestOption) (resp *requests.Response, err error) { data := options[0].Data jsondata := map[string]interface{}{ "challenge": base64.StdEncoding.EncodeToString(data.([]byte)), "key-system": "com.widevine.alpha", "uri": "data:;base64," + preCtx.Value("pssh").(string), "adamId": preCtx.Value("adamId").(string), "isLibrary": false, "user-initiated": true, } options[0].Data = nil options[0].Json = jsondata resp, err = cl.Request(preCtx, method, href, options...) if err != nil { fmt.Println(err) } return } func AfterRequest(Response *requests.Response) ([]byte, error) { var ResponseData PlaybackLicense _, err := Response.Json(&ResponseData) if err != nil { return nil, fmt.Errorf("failed to parse response: %v", err) } if ResponseData.ErrorCode != 0 || ResponseData.Status != 0 { return nil, fmt.Errorf("error code: %d", ResponseData.ErrorCode) } License, err := base64.StdEncoding.DecodeString(ResponseData.License) if err != nil { return nil, fmt.Errorf("failed to decode license: %v", err) } return License, nil } 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, } jsonData, err := json.Marshal(postData) if err != nil { fmt.Println("Error encoding JSON:", err) return "", "", err } req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(jsonData))) if err != nil { fmt.Println("Error creating request:", err) return "", "", err } req.Header.Set("Content-Type", "application/json") req.Header.Set("Origin", "https://music.apple.com") req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36") req.Header.Set("Referer", "https://music.apple.com/") req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", authtoken)) req.Header.Set("x-apple-music-user-token", mutoken) // 创建 HTTP 客户端 //client := &http.Client{} resp, err := http.DefaultClient.Do(req) // 发送请求 //resp, err := client.Do(req) if err != nil { fmt.Println("Error sending request:", err) return "", "", err } defer resp.Body.Close() //fmt.Println("Response Status:", resp.Status) obj := new(Songlist) err = json.NewDecoder(resp.Body).Decode(&obj) if err != nil { fmt.Println("json err:", err) return "", "", err } if len(obj.List) > 0 { if mvmode { return obj.List[0].HlsPlaylistUrl, "", nil } // 遍历 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, false) if err != nil { return "", "", err } return fileurl, kidBase64, nil } continue } } return "", "", nil } 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"` } `json:"assets"` } `json:"songList"` Status int `json:"status"` } func extractKidBase64(b string, mvmode bool) (string, string, error) { resp, err := http.Get(b) if err != nil { return "", "", err } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { return "", "", errors.New(resp.Status) } body, err := io.ReadAll(resp.Body) if err != nil { return "", "", err } masterString := string(body) from, listType, err := m3u8.DecodeFrom(strings.NewReader(masterString), true) if err != nil { return "", "", err } var kidbase64 string var urlBuilder strings.Builder if listType == m3u8.MEDIA { mediaPlaylist := from.(*m3u8.MediaPlaylist) if mediaPlaylist.Key != nil { split := strings.Split(mediaPlaylist.Key.URI, ",") kidbase64 = split[1] lastSlashIndex := strings.LastIndex(b, "/") // 截取最后一个斜杠之前的部分 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, urlBuilder.String(), nil } func extsong(b string) bytes.Buffer { resp, err := http.Get(b) if err != nil { fmt.Printf("下载文件失败: %v\n", err) } defer resp.Body.Close() var buffer bytes.Buffer bar := progressbar.NewOptions64( resp.ContentLength, progressbar.OptionClearOnFinish(), progressbar.OptionSetElapsedTime(false), progressbar.OptionSetPredictTime(false), progressbar.OptionShowElapsedTimeOnFinish(), progressbar.OptionShowCount(), progressbar.OptionEnableColorCodes(true), progressbar.OptionShowBytes(true), progressbar.OptionSetDescription("Downloading..."), progressbar.OptionSetTheme(progressbar.Theme{ Saucer: "", SaucerHead: "", SaucerPadding: "", BarStart: "", BarEnd: "", }), ) io.Copy(io.MultiWriter(&buffer, bar), resp.Body) return buffer } 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, fileurl, err = extractKidBase64(trackpath, true) 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) ctx = context.WithValue(ctx, "adamId", adamId) pssh, err := getPSSH("", kidBase64) //fmt.Println(pssh) if err != nil { fmt.Println(err) return "", err } headers := map[string]interface{}{ "authorization": "Bearer " + authtoken, "x-apple-music-user-token": mutoken, } client, _ := requests.NewClient(nil, requests.ClientOption{ Headers: headers, }) key := key.Key{ ReqCli: client, BeforeRequest: BeforeRequest, AfterRequest: AfterRequest, } key.CdmInit() 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 } if mvmode { keyAndUrls := "1:" + keystr + ";" + fileurl return keyAndUrls, nil } body := extsong(fileurl) fmt.Print("Downloaded\n") //bodyReader := bytes.NewReader(body) var buffer bytes.Buffer err = DecryptMP4(&body, keybt, &buffer) if err != nil { fmt.Print("Decryption failed\n") return "", err } else { fmt.Print("Decrypted\n") } // create output file ofh, err := os.Create(trackpath) if err != nil { fmt.Printf("创建文件失败: %v\n", err) return "", err } defer ofh.Close() _, err = ofh.Write(buffer.Bytes()) if err != nil { fmt.Printf("写入文件失败: %v\n", err) return "", err } return "", nil } 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 } if resp.StatusCode != http.StatusOK { fmt.Printf("链接 %s 响应失败:%v\n", url, resp.Status) return errors.New(resp.Status) } // 将响应体写入输出文件 _, 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(), filepath.Base(savePath)) cmd1.Dir = filepath.Dir(savePath) //设置mp4decrypt的工作目录以解决中文路径错误 outlog, err := cmd1.CombinedOutput() if 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 { // Initialization inMp4, err := mp4.DecodeFile(r) if err != nil { return fmt.Errorf("failed to decode file: %w", err) } if !inMp4.IsFragmented() { return errors.New("file is not fragmented") } // Handle init segment if inMp4.Init == nil { return errors.New("no init part of file") } decryptInfo, err := mp4.DecryptInit(inMp4.Init) if err != nil { return fmt.Errorf("failed to decrypt init: %w", err) } if err = inMp4.Init.Encode(w); err != nil { return fmt.Errorf("failed to write init: %w", err) } // Decode segments for _, seg := range inMp4.Segments { if err = mp4.DecryptSegment(seg, decryptInfo, key); err != nil { if err.Error() == "no senc box in traf" { // No SENC box, skip decryption for this segment as samples can have // unencrypted segments followed by encrypted segments. See: // https://github.com/iyear/gowidevine/pull/26#issuecomment-2385960551 err = nil } else { return fmt.Errorf("failed to decrypt segment: %w", err) } } if err = seg.Encode(w); err != nil { return fmt.Errorf("failed to encode segment: %w", err) } } return nil }