Files
apple-music-downloader/utils/runv3/runv3.go
2025-03-06 08:38:15 +08:00

512 lines
14 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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"
"sync"
"time"
"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 "", "", errors.New("Unavailable")
}
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()
var keybt []byte
if strings.Contains(adamId, "ra.") {
keystr, keybt, err = key.GetKey(ctx, "https://play.itunes.apple.com/WebObjects/MZPlay.woa/web/radio/versions/1/license", pssh, nil)
if err != nil {
fmt.Println(err)
return "", err
}
} else {
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 os.Remove(tempFile.Name())
defer tempFile.Close()
// 创建上下文用于取消所有下载任务
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 初始化进度条
bar := progressbar.DefaultBytes(-1, "Downloading...")
barWriter := io.MultiWriter(tempFile, bar)
// 预先创建所有管道
pipeReaders := make([]*io.PipeReader, len(urls))
pipeWriters := make([]*io.PipeWriter, len(urls))
for i := range urls {
pr, pw := io.Pipe()
pipeReaders[i] = pr
pipeWriters[i] = pw
}
// 控制并发数(使用空结构体节省内存)
sem := make(chan struct{}, 5)
var wg sync.WaitGroup
// 创建带超时的HTTP Client
client := &http.Client{
Timeout: 30 * time.Second,
}
// 启动下载任务
go func() {
for i, url := range urls {
select {
case <-ctx.Done():
return // 上下文已取消,直接返回
default:
sem <- struct{}{} // 获取信号量
wg.Add(1)
go func(i int, url string, pw *io.PipeWriter) {
defer func() {
<-sem // 释放信号量
wg.Done()
}()
// 创建带上下文的请求
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
pw.CloseWithError(err)
fmt.Printf("创建请求失败: %v\n", err)
return
}
resp, err := client.Do(req)
if err != nil {
pw.CloseWithError(err)
fmt.Printf("下载失败: %v\n", err)
return
}
defer resp.Body.Close()
// 检查HTTP状态码
if resp.StatusCode != http.StatusOK {
err := fmt.Errorf("非200状态码: %d", resp.StatusCode)
pw.CloseWithError(err)
fmt.Printf("下载失败: %v\n", err)
return
}
// 将响应体复制到管道
if _, err := io.Copy(pw, resp.Body); err != nil {
pw.CloseWithError(err)
} else {
pw.Close() // 正常关闭
}
}(i, url, pipeWriters[i])
}
}
}()
// 按顺序写入文件
for i := 0; i < len(urls); i++ {
if _, err := io.Copy(barWriter, pipeReaders[i]); err != nil {
cancel() // 取消所有下载任务
fmt.Printf("写入第 %d 部分失败: %v\n", i+1, err)
return err
}
pipeReaders[i].Close() // 关闭当前读取端
}
// 等待所有下载任务完成
wg.Wait()
// 显式关闭文件defer会再次调用但重复关闭是安全的
if err := tempFile.Close(); err != nil {
fmt.Printf("关闭临时文件失败: %v\n", err)
return err
}
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
}