427 lines
12 KiB
Go
427 lines
12 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"
|
|
"os/exec"
|
|
)
|
|
|
|
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 mvmode {
|
|
return obj.List[0].HlsPlaylistUrl, "", nil
|
|
}
|
|
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, 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(), savePath)
|
|
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
|
|
}
|