2025-01-15 15:55:30 +08:00

358 lines
10 KiB
Go

package runv3
import (
"context"
"encoding/base64"
"fmt"
"github.com/gospider007/requests"
"google.golang.org/protobuf/proto"
//"log/slog"
"os"
cdm "main/utils/runv3/cdm"
key "main/utils/runv3/key"
"github.com/Eyevinn/mp4ff/mp4"
"bytes"
"io"
"errors"
//"io/ioutil"
"net/http"
"encoding/json"
"github.com/grafov/m3u8"
"strings"
"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) (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 {
// 遍历 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)
if err != nil {
return "", "", err
}
return fileurl, kidBase64, nil
}
continue
}
}
return "", "", nil
}
type Songlist struct {
List []struct {
Hlsurl string `json:"hls-key-cert-url"`
Assets []struct {
Flavor string `json:"flavor"`
URL string `json:"URL"`
}`json:"assets"`
}`json:"songList"`
Status int `json:"status"`
}
func extractKidBase64(b string) (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 fileurl string
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, "/")
// 截取最后一个斜杠之前的部分
fileurl = b[:lastSlashIndex] + "/" + mediaPlaylist.Map.URI
//fmt.Println("Extracted URI:", mediaPlaylist.Map.URI)
} else {
fmt.Println("No key information found")
}
} else {
fmt.Println("Not a media playlist")
}
return kidbase64, fileurl, 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)
if err != nil {
fmt.Printf("读取文件失败: %v\n", err)
}
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
}
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()
_, 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
}
fmt.Print("Downloading...\n")
body := extsong(fileurl)
//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
}
// 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)
// }
// 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
}