mirror of
https://github.com/zhaarey/apple-music-downloader.git
synced 2025-10-23 15:11:05 +00:00
add acc-lc dl
This commit is contained in:
274
utils/runv3/cdm/cdm.go
Normal file
274
utils/runv3/cdm/cdm.go
Normal file
@@ -0,0 +1,274 @@
|
||||
package wv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rsa"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"github.com/aead/cmac"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"lukechampine.com/frand"
|
||||
"math"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CDM struct {
|
||||
privateKey *rsa.PrivateKey
|
||||
clientID []byte
|
||||
sessionID [32]byte
|
||||
|
||||
widevineCencHeader WidevineCencHeader
|
||||
signedDeviceCertificate SignedDeviceCertificate
|
||||
privacyMode bool
|
||||
}
|
||||
|
||||
type Key struct {
|
||||
ID []byte
|
||||
Type License_KeyContainer_KeyType
|
||||
Value []byte
|
||||
}
|
||||
|
||||
// Creates a new CDM object with the specified device information.
|
||||
func NewCDM(privateKey string, clientID []byte, initData []byte) (CDM, error) {
|
||||
block, _ := pem.Decode([]byte(privateKey))
|
||||
if block == nil || block.Type != "RSA PRIVATE KEY" {
|
||||
return CDM{}, errors.New("failed to decode device private key")
|
||||
}
|
||||
keyParsed, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return CDM{}, err
|
||||
}
|
||||
|
||||
var widevineCencHeader WidevineCencHeader
|
||||
if len(initData) < 32 {
|
||||
return CDM{}, errors.New("initData not long enough")
|
||||
}
|
||||
if err := proto.Unmarshal(initData[32:], &widevineCencHeader); err != nil {
|
||||
return CDM{}, err
|
||||
}
|
||||
|
||||
sessionID := func() (s [32]byte) {
|
||||
c := []byte("ABCDEF0123456789")
|
||||
for i := 0; i < 16; i++ {
|
||||
s[i] = c[frand.Intn(len(c))]
|
||||
}
|
||||
s[16] = '0'
|
||||
s[17] = '1'
|
||||
for i := 18; i < 32; i++ {
|
||||
s[i] = '0'
|
||||
}
|
||||
return s
|
||||
}()
|
||||
|
||||
return CDM{
|
||||
privateKey: keyParsed,
|
||||
clientID: clientID,
|
||||
|
||||
widevineCencHeader: widevineCencHeader,
|
||||
|
||||
sessionID: sessionID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Creates a new CDM object using the default device configuration.
|
||||
func NewDefaultCDM(initData []byte) (CDM, error) {
|
||||
return NewCDM(DefaultPrivateKey, DefaultClientID, initData)
|
||||
}
|
||||
|
||||
// Sets a device certificate. This is makes generating the license request
|
||||
// more complicated but is supported. This is usually not necessary for most
|
||||
// Widevine applications.
|
||||
func (c *CDM) SetServiceCertificate(certData []byte) error {
|
||||
var message SignedMessage
|
||||
if err := proto.Unmarshal(certData, &message); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := proto.Unmarshal(message.Msg, &c.signedDeviceCertificate); err != nil {
|
||||
return err
|
||||
}
|
||||
c.privacyMode = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *CDM) GetServiceCertificate() *SignedDeviceCertificate {
|
||||
|
||||
return &c.signedDeviceCertificate
|
||||
}
|
||||
|
||||
// Generates the license request data. This is sent to the license server via
|
||||
// HTTP POST and the server in turn returns the license response.
|
||||
func (c *CDM) GetLicenseRequest() ([]byte, error) {
|
||||
var licenseRequest SignedLicenseRequest
|
||||
licenseRequest.Msg = new(LicenseRequest)
|
||||
licenseRequest.Msg.ContentId = new(LicenseRequest_ContentIdentification)
|
||||
licenseRequest.Msg.ContentId.CencId = new(LicenseRequest_ContentIdentification_CENC)
|
||||
|
||||
// this is probably really bad for the GC but protobuf uses pointers for optional
|
||||
// fields so it is necessary and this is not a long running program
|
||||
{
|
||||
v := SignedLicenseRequest_LICENSE_REQUEST
|
||||
licenseRequest.Type = &v
|
||||
}
|
||||
|
||||
licenseRequest.Msg.ContentId.CencId.Pssh = &c.widevineCencHeader
|
||||
|
||||
{
|
||||
v := LicenseType_DEFAULT
|
||||
licenseRequest.Msg.ContentId.CencId.LicenseType = &v
|
||||
}
|
||||
|
||||
licenseRequest.Msg.ContentId.CencId.RequestId = c.sessionID[:]
|
||||
|
||||
{
|
||||
v := LicenseRequest_NEW
|
||||
licenseRequest.Msg.Type = &v
|
||||
}
|
||||
|
||||
{
|
||||
v := uint32(time.Now().Unix())
|
||||
licenseRequest.Msg.RequestTime = &v
|
||||
}
|
||||
|
||||
{
|
||||
v := ProtocolVersion_CURRENT
|
||||
licenseRequest.Msg.ProtocolVersion = &v
|
||||
}
|
||||
|
||||
{
|
||||
v := uint32(frand.Uint64n(math.MaxUint32))
|
||||
licenseRequest.Msg.KeyControlNonce = &v
|
||||
}
|
||||
|
||||
if c.privacyMode {
|
||||
pad := func(data []byte, blockSize int) []byte {
|
||||
padlen := blockSize - (len(data) % blockSize)
|
||||
if padlen == 0 {
|
||||
padlen = blockSize
|
||||
}
|
||||
return append(data, bytes.Repeat([]byte{byte(padlen)}, padlen)...)
|
||||
}
|
||||
const blockSize = 16
|
||||
|
||||
var cidKey, cidIV [blockSize]byte
|
||||
frand.Read(cidKey[:])
|
||||
frand.Read(cidIV[:])
|
||||
|
||||
block, err := aes.NewCipher(cidKey[:])
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
paddedClientID := pad(c.clientID, blockSize)
|
||||
encryptedClientID := make([]byte, len(paddedClientID))
|
||||
cipher.NewCBCEncrypter(block, cidIV[:]).CryptBlocks(encryptedClientID, paddedClientID)
|
||||
|
||||
servicePublicKey, err := x509.ParsePKCS1PublicKey(c.signedDeviceCertificate.XDeviceCertificate.PublicKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
encryptedCIDKey, err := rsa.EncryptOAEP(sha1.New(), frand.Reader, servicePublicKey, cidKey[:], nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
licenseRequest.Msg.EncryptedClientId = new(EncryptedClientIdentification)
|
||||
{
|
||||
v := string(c.signedDeviceCertificate.XDeviceCertificate.ServiceId)
|
||||
licenseRequest.Msg.EncryptedClientId.ServiceId = &v
|
||||
}
|
||||
licenseRequest.Msg.EncryptedClientId.ServiceCertificateSerialNumber = c.signedDeviceCertificate.XDeviceCertificate.SerialNumber
|
||||
licenseRequest.Msg.EncryptedClientId.EncryptedClientId = encryptedClientID
|
||||
licenseRequest.Msg.EncryptedClientId.EncryptedClientIdIv = cidIV[:]
|
||||
licenseRequest.Msg.EncryptedClientId.EncryptedPrivacyKey = encryptedCIDKey
|
||||
} else {
|
||||
licenseRequest.Msg.ClientId = new(ClientIdentification)
|
||||
if err := proto.Unmarshal(c.clientID, licenseRequest.Msg.ClientId); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
data, err := proto.Marshal(licenseRequest.Msg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
hash := sha1.Sum(data)
|
||||
if licenseRequest.Signature, err = rsa.SignPSS(frand.Reader, c.privateKey, crypto.SHA1, hash[:], &rsa.PSSOptions{SaltLength: rsa.PSSSaltLengthEqualsHash}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return proto.Marshal(&licenseRequest)
|
||||
}
|
||||
|
||||
// Retrieves the keys from the license response data. These keys can be
|
||||
// used to decrypt the DASH-MP4.
|
||||
func (c *CDM) GetLicenseKeys(licenseRequest []byte, licenseResponse []byte) (keys []Key, err error) {
|
||||
var license SignedLicense
|
||||
if err = proto.Unmarshal(licenseResponse, &license); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var licenseRequestParsed SignedLicenseRequest
|
||||
if err = proto.Unmarshal(licenseRequest, &licenseRequestParsed); err != nil {
|
||||
return
|
||||
}
|
||||
licenseRequestMsg, err := proto.Marshal(licenseRequestParsed.Msg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sessionKey, err := rsa.DecryptOAEP(sha1.New(), frand.Reader, c.privateKey, license.SessionKey, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
sessionKeyBlock, err := aes.NewCipher(sessionKey)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
encryptionKey := []byte{1, 'E', 'N', 'C', 'R', 'Y', 'P', 'T', 'I', 'O', 'N', 0}
|
||||
encryptionKey = append(encryptionKey, licenseRequestMsg...)
|
||||
encryptionKey = append(encryptionKey, []byte{0, 0, 0, 0x80}...)
|
||||
encryptionKeyCmac, err := cmac.Sum(encryptionKey, sessionKeyBlock, sessionKeyBlock.BlockSize())
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
encryptionKeyCipher, err := aes.NewCipher(encryptionKeyCmac)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
unpad := func(b []byte) []byte {
|
||||
if len(b) == 0 {
|
||||
return b
|
||||
}
|
||||
// pks padding is designed so that the value of all the padding bytes is
|
||||
// the number of padding bytes repeated so to figure out how many
|
||||
// padding bytes there are we can just look at the value of the last
|
||||
// byte
|
||||
// i.e if there are 6 padding bytes then it will look at like
|
||||
// <data> 0x6 0x6 0x6 0x6 0x6 0x6
|
||||
count := int(b[len(b)-1])
|
||||
return b[0 : len(b)-count]
|
||||
}
|
||||
for _, key := range license.Msg.Key {
|
||||
decrypter := cipher.NewCBCDecrypter(encryptionKeyCipher, key.Iv)
|
||||
decryptedKey := make([]byte, len(key.Key))
|
||||
decrypter.CryptBlocks(decryptedKey, key.Key)
|
||||
keys = append(keys, Key{
|
||||
ID: key.Id,
|
||||
Type: *key.Type,
|
||||
Value: unpad(decryptedKey),
|
||||
})
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
24
utils/runv3/cdm/consts.go
Normal file
24
utils/runv3/cdm/consts.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package wv
|
||||
|
||||
import "encoding/base64"
|
||||
|
||||
var DefaultPrivateKey string
|
||||
var DefaultClientID []byte
|
||||
|
||||
func InitConstants() {
|
||||
|
||||
DefaultPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA2bO3yvFwNnIHsbDl3MTjKdDsiBWsuZWOGVxInFWAVMp+nffG\nYlquTKpJurEry95yprcRB3hYhvA5ghsACidcWPDEPVqqRZ7YXLevyUA+Sn2Jxpvt\nOcwyFHbSwruNxprWOkHCT774O4L/wJUt5x2C4iFCrJByjw0omN8u+EHdavvH7ZPn\nb3/EZp/cpZa9/+HOkutvBHBvaPp18F8JQhzUQ9MwLuDFTr+QLDB5+Y57Je2tNYDK\nxD1K+Ed5Ja0A4OKhPKIwPwPre0nt5scjLba3LSAKtKxiGqFtWO4U7Tf1YrdjJv2o\n9o8Sf8qcnbpzvQ4KwFqehuJnB7+W7mdJJw12PQIDAQABAoIBACE32wOMc6LbI3Fp\nnKljIYZv6qeZJxHqUBRukGXKZhqKC2fvNsYrMA1irn1eK2CgQL5PkLmjE18DqMLB\ne/AQsXagxlDWVMTqx/jdzmTW+KpFHZDAmiIHllypBN/R3oA/gBDDl/KzIQ1zn7Kz\nEJ4DUsVObe4G3HQXfepVo8Udx7tbB7X6wHe2kEgFyY3lPdvubik0C4t4ipSD79y7\nSfW7XVA5XUQmqN4U2kWM0uSwzd4BA7hqyScJsygf6KgpMWPS2xFZEZQRUpYcBH48\nE7YqNrrlYP3yaQ+9Jx56kKS0mvv3vUXS7AfUbU8CiHwD9I3BGwswEUueOGGVeXbx\ntFF8s8ECgYEA97BDcL/bt+r3qJF0dxtMB5ZngJbFx9RdsblYepVpblr2UfxnFttO\nPoNSKa4W36HuDsun49dkaoABJWdtZs2Hy6q+xvEgozvhMaBVE3spnWnzCT1yTMYL\nG02uDEl0dPiTg116bVElaswtqMXvnnpbOTMTe7Ig9sWiUW/GH9RM+N8CgYEA4QHb\n+OA0BfczbVQP9B+plt4mAuu4BDm4GPwq1yXOWo3Ct8Ik+HeY1hqOObpfyQMAza+E\ne/kP6W8vXpiElGrmiUbTXK4Rzmf+yYeOrvl3D80bFq4GtDNAIQD3jpj6zjlT+Gzw\nI501gRx5iPl4fSccRSdpoeri7F9ANtc6EEGFyGMCgYEAjMznWYXHGkL47BtbkIW0\n769BQSj0X4dKh8gsEusylugglDSeSbD7RrASGd175T7A/CorU2rTC3OesyubVlBJ\n/K4gaykRe5mDh1l0Y3GlE3XyEXObsSb3k1rSMOvkxsWz3X5bJR923MIaxpFWiMlX\naCmvzqZQ9NceUZrvjpJ5+xMCgYAJa8KCESEcftUwZqykVA8Nug9tX+E8jA4hPa2t\nhG+3augUOZTCsn87t7Dsydjo2a9W7Vpmtm7sHzOkik5CyJcOeGCxKLimI8SPO5XF\nzbwmdTgFIxQ0x1CQETJMTityJwRVCnqjgxmSZlbQXWGmG9UbMCNEHEmUDAjsQuaz\nd4racQKBgQDR1Y2kalvleYGrhwcA8LTnIh0rYEfAt9YxNmTi5qDKf5QPvUP2v+WO\nfSB5coUqR8LBweHE5V8JgFt74fdLBqZV/k2z/dI0r+EQWmpZ2uPEC0Khk/Sb9iRD\nfH7at3PMusrkwZCGZ8beFEAr6icXclV08nPCNGB6WckacfzpAj8Azg==\n-----END RSA PRIVATE KEY-----"
|
||||
DefaultClientdIDBase64 := "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"
|
||||
DefaultClientID, _ = base64.StdEncoding.DecodeString(DefaultClientdIDBase64)
|
||||
|
||||
// DefaultPrivateKeyBuffer, err := ioutil.ReadFile("device_private_key")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
// DefaultPrivateKey = string(DefaultPrivateKeyBuffer)
|
||||
//
|
||||
// DefaultClientID, err = ioutil.ReadFile("device_client_id_blob")
|
||||
// if err != nil {
|
||||
// panic(err)
|
||||
// }
|
||||
}
|
||||
16
utils/runv3/cdm/pssh.go
Normal file
16
utils/runv3/cdm/pssh.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package wv
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
func GetCertData(client *http.Client, licenseURL string) ([]byte, error) {
|
||||
response, err := client.Post(licenseURL, "application/x-www-form-urlencoded", bytes.NewReader([]byte{0x08, 0x04}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
return io.ReadAll(response.Body)
|
||||
}
|
||||
5729
utils/runv3/cdm/wv_proto2.pb.go
Normal file
5729
utils/runv3/cdm/wv_proto2.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
73
utils/runv3/key/key.go
Normal file
73
utils/runv3/key/key.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package wv
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"github.com/gospider007/requests"
|
||||
"log/slog"
|
||||
"main/utils/runv3/cdm"
|
||||
)
|
||||
|
||||
type Key struct {
|
||||
ReqCli *requests.Client
|
||||
BeforeRequest func(cl *requests.Client, preCtx context.Context, method string, href string, options ...requests.RequestOption) (resp *requests.Response, err error)
|
||||
AfterRequest func(*requests.Response) ([]byte, error)
|
||||
}
|
||||
|
||||
func (w *Key) CdmInit() {
|
||||
wv.InitConstants()
|
||||
}
|
||||
func (w *Key) GetKey(ctx context.Context, licenseServerURL string, PSSH string, headers map[string][]string) (string, []byte, error) {
|
||||
initData, err := base64.StdEncoding.DecodeString(PSSH)
|
||||
var keybt []byte
|
||||
if err != nil {
|
||||
slog.Error("pssh decode error: %v", err)
|
||||
return "", keybt, err
|
||||
}
|
||||
cdm, err := wv.NewDefaultCDM(initData)
|
||||
if err != nil {
|
||||
slog.Error("cdm init error: %v", err)
|
||||
return "", keybt, err
|
||||
}
|
||||
licenseRequest, err := cdm.GetLicenseRequest()
|
||||
if err != nil {
|
||||
slog.Error("license request error: %v", err)
|
||||
return "", keybt, err
|
||||
}
|
||||
var response *requests.Response
|
||||
if w.BeforeRequest != nil {
|
||||
response, err = w.BeforeRequest(w.ReqCli, ctx, "post", licenseServerURL, requests.RequestOption{
|
||||
Data: licenseRequest,
|
||||
})
|
||||
} else {
|
||||
response, err = w.ReqCli.Request(nil, "post", licenseServerURL, requests.RequestOption{
|
||||
Data: licenseRequest,
|
||||
})
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
slog.Error("license request error: %s", err)
|
||||
return "", keybt, err
|
||||
}
|
||||
var licenseResponse []byte
|
||||
if w.AfterRequest != nil {
|
||||
licenseResponse, err = w.AfterRequest(response)
|
||||
if err != nil {
|
||||
return "", keybt, err
|
||||
}
|
||||
} else {
|
||||
licenseResponse = response.Content()
|
||||
}
|
||||
keys, err := cdm.GetLicenseKeys(licenseRequest, licenseResponse)
|
||||
command := ""
|
||||
|
||||
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)
|
||||
keybt = key.Value
|
||||
}
|
||||
}
|
||||
return command, keybt, nil
|
||||
}
|
||||
357
utils/runv3/runv3.go
Normal file
357
utils/runv3/runv3.go
Normal file
@@ -0,0 +1,357 @@
|
||||
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]any{
|
||||
"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
|
||||
}
|
||||
Reference in New Issue
Block a user