Files
djk/internal/app/provider_hongguo_playback.go

202 lines
6.5 KiB
Go

package app
import (
"context"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"math/bits"
"net/url"
"regexp"
"strconv"
"strings"
)
const hongguoPlaybackAPI = "https://djapi.999888456.xyz/api/hongguo/play"
var hongguoNumericID = regexp.MustCompile(`^[0-9]{1,32}$`)
var hongguoQualityNumber = regexp.MustCompile(`[0-9]+`)
type hongguoPlaybackReference struct {
ContentType int `json:"content_type"`
FromVideoID string `json:"from_video_id"`
SeriesID string `json:"series_id"`
VideoID string `json:"vid"`
VideoPlatform int `json:"video_platform"`
}
type hongguoPlaybackResponse struct {
Parse json.RawMessage `json:"parse"`
JX json.RawMessage `json:"jx"`
KeyURLs []struct {
Name string `json:"name"`
URL string `json:"src"`
KeyID string `json:"kid"`
SpadeA string `json:"spade_a"`
} `json:"key_urls"`
}
func (d *Downloader) resolveHongguoPlaybackAPI(ctx context.Context, seriesID, videoID string) (providerMedia, error) {
if !hongguoNumericID.MatchString(seriesID) || !hongguoNumericID.MatchString(videoID) {
return providerMedia{}, errors.New("红果播放请求缺少有效的剧集 ID")
}
reference, err := json.Marshal(hongguoPlaybackReference{ContentType: 1004, SeriesID: seriesID, VideoID: videoID, VideoPlatform: 3})
if err != nil {
return providerMedia{}, err
}
query := url.Values{"id": {base64.StdEncoding.EncodeToString(reference)}}
body, err := d.fetchProviderText(ctx, hongguoPlaybackAPI+"?"+query.Encode(), hongguoBaseURL+"/")
if err != nil {
return providerMedia{}, err
}
decoded, err := decodeHongguoPlaybackResponse(body)
if err != nil {
return providerMedia{}, err
}
var response hongguoPlaybackResponse
if err := json.Unmarshal(decoded, &response); err != nil {
return providerMedia{}, errors.New("红果备用播放接口返回了无效数据")
}
for _, flag := range []json.RawMessage{response.Parse, response.JX} {
switch strings.TrimSpace(string(flag)) {
case "", "null", "false", "0", `"0"`, `""`:
default:
return providerMedia{}, errors.New("红果备用播放接口没有返回直接媒体地址")
}
}
var selected providerMedia
bestQuality := -1
var keyErr error
var variants []providerMedia
for _, option := range response.KeyURLs {
mediaURL := strings.TrimSpace(option.URL)
if len(mediaURL) > 8192 || !isProviderHTTPMediaURL(mediaURL) {
continue
}
keyID, err := hex.DecodeString(strings.TrimSpace(option.KeyID))
if err != nil || len(keyID) != aes.BlockSize {
keyErr = errors.New("红果媒体密钥标识无效")
continue
}
key, err := hongguoContentKey(option.SpadeA)
if err != nil {
keyErr = err
continue
}
quality, _ := strconv.Atoi(hongguoQualityNumber.FindString(option.Name))
media := providerMedia{URL: mediaURL, Referer: "https://novel.snssdk.com/", CENCKey: key, Quality: quality}
variants = append(variants, media)
if selected.URL == "" || quality > bestQuality {
selected = media
bestQuality = quality
}
}
if selected.URL != "" {
selected.Variants = variants
return selected, nil
}
if keyErr != nil {
return providerMedia{}, keyErr
}
return providerMedia{}, errors.New("红果备用播放接口未返回该集可用的媒体和密钥,请稍后重试或确认该集是否仍可访问")
}
func decodeHongguoPlaybackResponse(body string) ([]byte, error) {
text := strings.TrimSpace(body)
if !strings.HasPrefix(text, "v2.") {
return []byte(text), nil
}
parts := strings.SplitN(text, ".", 3)
if len(parts) != 3 || len(parts[1]) <= 4 || len(parts[1]) > 1028 {
return nil, errors.New("红果备用接口响应密钥无效")
}
encoded, err := hex.DecodeString(parts[1][4:])
if err != nil || len(encoded) < 32 {
return nil, errors.New("红果备用接口响应密钥无效")
}
mask := [...]byte{104, 64, 70, 166, 190, 168, 143, 130, 225, 254, 251, 217, 196, 34, 45, 60, 29, 20, 103, 105}
material := make([]byte, len(encoded))
for index, current := range encoded {
previous := byte(109)
if index > 0 {
previous = encoded[index-1]
}
slot := index % len(mask)
salt := mask[slot] ^ byte(90+13*slot) ^ 85
shifted := byte(int(current) + 215 - 11*index)
material[index] = previous ^ salt ^ bits.RotateLeft8(shifted, 3)
}
ciphertext, err := decodeHongguoBase64(parts[2])
if err != nil || len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 {
return nil, errors.New("红果备用接口加密响应无效")
}
block, err := aes.NewCipher(material[:16])
if err != nil {
return nil, err
}
plain := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, material[16:32]).CryptBlocks(plain, ciphertext)
unpadded, err := pkcs7Unpad(plain, aes.BlockSize)
if err != nil {
return nil, errors.New("红果备用接口响应解密失败")
}
return unpadded, nil
}
func decodeHongguoBase64(value string) ([]byte, error) {
value = strings.TrimSpace(value)
decoded, err := base64.StdEncoding.Strict().DecodeString(value)
if err == nil {
return decoded, nil
}
return base64.RawStdEncoding.Strict().DecodeString(value)
}
func hongguoContentKey(value string) ([]byte, error) {
if len(value) > 1024 {
return nil, errors.New("红果媒体密钥数据过长")
}
raw, err := decodeHongguoBase64(value)
if err != nil || len(raw) < 3 {
return nil, errors.New("红果媒体密钥编码无效")
}
tagLength := int(raw[0]^raw[1]^raw[2]) - 48
contentLength := len(raw) - tagLength - 1
if tagLength < 1 || contentLength < 33 || contentLength >= len(raw) {
return nil, errors.New("红果媒体密钥结构无效")
}
seed := raw[len(raw)-tagLength-2] ^ raw[len(raw)-tagLength-1]
tag := make([]byte, tagLength)
for index := range tag {
tag[index] = raw[len(raw)-tagLength+index] ^ seed
}
if string(tag) == "app_v2" || string(tag) == "web_v2" {
return nil, errors.New("红果媒体密钥版本暂不支持")
}
decoded := make([]byte, contentLength)
previousEven, previousOdd := byte(250), byte(85)
for index, current := range raw[1 : 1+contentLength] {
previous := previousEven
if index%2 == 0 {
previousEven = current
} else {
previous = previousOdd
previousOdd = current
}
decoded[index] = byte(int(previous^current) - 21 - bits.OnesCount(uint(index)))
}
padding, err := strconv.ParseUint(string(decoded[:1]), 36, 8)
if err != nil || contentLength-int(padding)-1 != 32 {
return nil, errors.New("红果媒体密钥内容无效")
}
key, err := hex.DecodeString(string(decoded[1:33]))
if err != nil || len(key) != aes.BlockSize {
return nil, fmt.Errorf("红果媒体密钥不是有效的 AES-128 密钥")
}
return key, nil
}