初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,243 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type legacyProtocol struct {
|
||||
InterfaceKey string `json:"interfaceKey"`
|
||||
ParamKey string `json:"paramKey"`
|
||||
ParamIV string `json:"paramIV"`
|
||||
}
|
||||
|
||||
type legacySessionState struct {
|
||||
Version int `json:"version"`
|
||||
Scope string `json:"scope"`
|
||||
DeviceID string `json:"deviceId"`
|
||||
Token string `json:"token"`
|
||||
Protocol legacyProtocol `json:"protocol"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type legacyAccess struct {
|
||||
legacyProtocol
|
||||
Token string
|
||||
DeviceID string
|
||||
Anonymous bool
|
||||
}
|
||||
|
||||
type legacyAPIClient struct {
|
||||
mu sync.Mutex
|
||||
loaded bool
|
||||
state legacySessionState
|
||||
access *legacyAccess
|
||||
pending chan struct{}
|
||||
err error
|
||||
retryAt time.Time
|
||||
}
|
||||
|
||||
func (d *Downloader) legacyClient() *legacyAPIClient {
|
||||
d.legacyOnce.Do(func() { d.legacy = &legacyAPIClient{} })
|
||||
return d.legacy
|
||||
}
|
||||
|
||||
func (d *Downloader) legacyCredentials(ctx context.Context) (legacyAccess, error) {
|
||||
client := d.legacyClient()
|
||||
for {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return legacyAccess{}, err
|
||||
}
|
||||
client.mu.Lock()
|
||||
if client.access != nil {
|
||||
access := *client.access
|
||||
client.mu.Unlock()
|
||||
return access, nil
|
||||
}
|
||||
if pending := client.pending; pending != nil {
|
||||
client.mu.Unlock()
|
||||
select {
|
||||
case <-pending:
|
||||
continue
|
||||
case <-ctx.Done():
|
||||
return legacyAccess{}, ctx.Err()
|
||||
}
|
||||
}
|
||||
if client.err != nil && time.Now().Before(client.retryAt) {
|
||||
err := client.err
|
||||
client.mu.Unlock()
|
||||
return legacyAccess{}, err
|
||||
}
|
||||
if !client.loaded {
|
||||
client.state = d.loadLegacySession()
|
||||
client.loaded = true
|
||||
}
|
||||
state := client.state
|
||||
pending := make(chan struct{})
|
||||
client.pending = pending
|
||||
client.mu.Unlock()
|
||||
|
||||
access, updated, err := d.bootstrapLegacySession(ctx, state)
|
||||
client.mu.Lock()
|
||||
client.state = updated
|
||||
client.err = err
|
||||
client.retryAt = time.Time{}
|
||||
if err == nil {
|
||||
client.access = &access
|
||||
} else if !errors.Is(err, context.Canceled) && !errors.Is(err, context.DeadlineExceeded) {
|
||||
client.retryAt = time.Now().Add(time.Minute)
|
||||
}
|
||||
client.pending = nil
|
||||
close(pending)
|
||||
client.mu.Unlock()
|
||||
return access, err
|
||||
}
|
||||
}
|
||||
|
||||
func (d *Downloader) legacySessionScope() string {
|
||||
return strings.TrimRight(firstNonEmpty(d.cfg.APIBase, defaultAPIBase), "/")
|
||||
}
|
||||
|
||||
func (d *Downloader) loadLegacySession() legacySessionState {
|
||||
empty := legacySessionState{Version: 1, Scope: d.legacySessionScope()}
|
||||
file, err := os.Open(filepath.Join(d.cfg.dataDirectory(), "huangguo-session.json"))
|
||||
if err != nil {
|
||||
return empty
|
||||
}
|
||||
defer file.Close()
|
||||
var state legacySessionState
|
||||
if json.NewDecoder(io.LimitReader(file, 16*1024)).Decode(&state) != nil || state.Version != 1 || state.Scope != empty.Scope ||
|
||||
!regexp.MustCompile(`^[A-Fa-f0-9]{16}[0-9]{13}$`).MatchString(state.DeviceID) || !validLegacyToken(state.Token) {
|
||||
return empty
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
func validLegacyToken(token string) bool {
|
||||
return len(token) <= 4096 && !strings.ContainsAny(token, "\r\n\t ")
|
||||
}
|
||||
|
||||
func (d *Downloader) saveLegacySession(state legacySessionState) error {
|
||||
directory := d.cfg.dataDirectory()
|
||||
if err := os.MkdirAll(directory, 0o755); err != nil {
|
||||
return err
|
||||
}
|
||||
file, err := os.CreateTemp(directory, ".huangguo-session-*.tmp")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(file.Name())
|
||||
if err := json.NewEncoder(file).Encode(state); err != nil {
|
||||
file.Close()
|
||||
return err
|
||||
}
|
||||
if err := file.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(file.Name(), filepath.Join(directory, "huangguo-session.json"))
|
||||
}
|
||||
|
||||
func (d *Downloader) bootstrapLegacySession(ctx context.Context, state legacySessionState) (legacyAccess, legacySessionState, error) {
|
||||
access := legacyAccess{Token: d.cfg.Token, Anonymous: d.cfg.Token == ""}
|
||||
protocol := state.Protocol
|
||||
needsProtocol := d.cfg.InterfaceKey == "" || d.cfg.ParamKey == "" || d.cfg.ParamIV == ""
|
||||
if needsProtocol && (!validLegacyProtocol(protocol) || time.Since(state.UpdatedAt) > 24*time.Hour) {
|
||||
script, err := d.fetchLegacyFrontendScript(ctx)
|
||||
if err != nil {
|
||||
return access, state, fmt.Errorf("获取黄果官网协议失败: %w", publicError(err))
|
||||
}
|
||||
protocol, err = parseLegacyProtocol(script)
|
||||
if err != nil {
|
||||
return access, state, err
|
||||
}
|
||||
state.Protocol, state.UpdatedAt = protocol, time.Now()
|
||||
}
|
||||
access.legacyProtocol = legacyProtocol{
|
||||
InterfaceKey: firstNonEmpty(d.cfg.InterfaceKey, protocol.InterfaceKey),
|
||||
ParamKey: firstNonEmpty(d.cfg.ParamKey, protocol.ParamKey),
|
||||
ParamIV: firstNonEmpty(d.cfg.ParamIV, protocol.ParamIV),
|
||||
}
|
||||
if !validLegacyProtocol(access.legacyProtocol) || !validLegacyToken(access.Token) {
|
||||
return access, state, errors.New("黄果 API 协议配置无效,请检查 sources.huangguo.legacyAPI")
|
||||
}
|
||||
if access.Anonymous {
|
||||
if state.DeviceID == "" {
|
||||
prefix := strings.ToUpper(randomHex(8))
|
||||
if prefix == "" {
|
||||
return access, state, errors.New("无法生成黄果访客标识")
|
||||
}
|
||||
state.DeviceID = prefix + strconv.FormatInt(time.Now().UnixMilli(), 10)
|
||||
}
|
||||
access.DeviceID, access.Token = state.DeviceID, state.Token
|
||||
if access.Token == "" {
|
||||
payload, err := d.legacyRequest(ctx, http.MethodPost, "/api/app/mine/login/h5", map[string]any{
|
||||
"devID": access.DeviceID, "sysType": "ios", "isAppStore": false,
|
||||
}, access)
|
||||
if err != nil {
|
||||
return access, state, fmt.Errorf("黄果访客登录失败: %w", err)
|
||||
}
|
||||
var login struct {
|
||||
Token string `json:"token"`
|
||||
}
|
||||
if json.Unmarshal(payload, &login) != nil || login.Token == "" || !validLegacyToken(login.Token) {
|
||||
return access, state, errors.New("黄果访客登录未返回有效令牌")
|
||||
}
|
||||
access.Token, state.Token = login.Token, login.Token
|
||||
}
|
||||
}
|
||||
if access.Anonymous {
|
||||
if err := d.saveLegacySession(state); err != nil {
|
||||
fmt.Println("黄果访客会话未能保存,本次会话仍可使用")
|
||||
}
|
||||
}
|
||||
return access, state, nil
|
||||
}
|
||||
|
||||
func validLegacyProtocol(protocol legacyProtocol) bool {
|
||||
return len(protocol.InterfaceKey) > 0 && len(protocol.InterfaceKey) <= 512 && len(protocol.ParamKey) == 16 && len(protocol.ParamIV) == 16
|
||||
}
|
||||
|
||||
func parseLegacyProtocol(script string) (legacyProtocol, error) {
|
||||
value := func(name string) string {
|
||||
pattern := `["']` + regexp.QuoteMeta(name) + `["']\s*[:,]\s*["']([^"'\\\r\n]+)["']`
|
||||
match := regexp.MustCompile(pattern).FindStringSubmatch(script)
|
||||
if len(match) == 2 {
|
||||
return match[1]
|
||||
}
|
||||
return ""
|
||||
}
|
||||
protocol := legacyProtocol{InterfaceKey: value("interfaceKey"), ParamKey: value("parameterKey"), ParamIV: value("parameterIv")}
|
||||
if !validLegacyProtocol(protocol) {
|
||||
return legacyProtocol{}, errors.New("黄果官网协议已变化,无法识别当前接口参数")
|
||||
}
|
||||
return protocol, nil
|
||||
}
|
||||
|
||||
func (d *Downloader) invalidateLegacyToken(access legacyAccess) {
|
||||
client := d.legacyClient()
|
||||
client.mu.Lock()
|
||||
defer client.mu.Unlock()
|
||||
if access.Anonymous && client.access != nil && client.access.Token == access.Token {
|
||||
client.state.Token = ""
|
||||
client.access = nil
|
||||
client.err = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (access legacyAccess) userAgent() string {
|
||||
if access.DeviceID == "" {
|
||||
return xUserAgent
|
||||
}
|
||||
return strings.Replace(xUserAgent, "DevID=00000000000000000000000000000", "DevID="+access.DeviceID, 1)
|
||||
}
|
||||
Reference in New Issue
Block a user