初始提交:短剧库源码(含转码优化 ultrafast)

This commit is contained in:
yvan
2026-09-17 19:09:08 +08:00
commit 07dc0f1059
247 changed files with 43242 additions and 0 deletions
+235
View File
@@ -0,0 +1,235 @@
package app
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
"time"
)
const hongguoAppBaseURL = "https://api5-normal-sinfonlineb.fqnovel.com"
const hongguoAppUserAgent = "com.phoenix.read/73532 (Linux; U; Android 16; zh_CN; 25053RT47C; Build/BP2A.250605.031.A3; Cronet/TTNetVersion:04657795 2026-01-23 QuicVersion:c67e9834 2025-09-08)"
type hongguoCatalogCursor struct {
Offset int `json:"offset"`
SessionID string `json:"sessionId,omitempty"`
LastID string `json:"lastId,omitempty"`
PageSignature string `json:"pageSignature,omitempty"`
Initialized bool `json:"initialized"`
Exhausted bool `json:"exhausted"`
UpdatedAt time.Time `json:"updatedAt"`
}
type hongguoCatalogState struct {
Version int `json:"version"`
DeviceID string `json:"deviceId"`
InstallID string `json:"installId"`
Feeds map[string]hongguoCatalogCursor `json:"feeds"`
}
type hongguoAppClient struct {
mu sync.Mutex
catalogMu sync.Mutex
baseURL string
state hongguoCatalogState
details map[string]hongguoDetailEntry
pending map[string]*hongguoDetailCall
searches map[string]hongguoSearchEntry
searchPending map[string]*hongguoSearchCall
danmaku map[string]hongguoDanmakuCacheEntry
danmakuPending map[string]*hongguoDanmakuCall
}
func (downloader *Downloader) hongguoClient() *hongguoAppClient {
downloader.hongguoOnce.Do(func() {
downloader.hongguo = &hongguoAppClient{
baseURL: hongguoAppBaseURL,
state: hongguoCatalogState{Version: 1, DeviceID: newHongguoDeviceID(), InstallID: newHongguoDeviceID(), Feeds: map[string]hongguoCatalogCursor{}},
details: map[string]hongguoDetailEntry{}, pending: map[string]*hongguoDetailCall{},
searches: map[string]hongguoSearchEntry{}, searchPending: map[string]*hongguoSearchCall{},
}
})
return downloader.hongguo
}
func cloneHongguoCatalogState(state *hongguoCatalogState) *hongguoCatalogState {
if state == nil {
return nil
}
cloned := *state
cloned.Feeds = make(map[string]hongguoCatalogCursor, len(state.Feeds))
for name, cursor := range state.Feeds {
cloned.Feeds[name] = cursor
}
return &cloned
}
func (downloader *Downloader) restoreHongguoCatalog(state *hongguoCatalogState) {
if state == nil || state.Version != 1 {
return
}
client := downloader.hongguoClient()
client.mu.Lock()
defer client.mu.Unlock()
if len(client.state.Feeds) != 0 {
return
}
cloned := cloneHongguoCatalogState(state)
if !hongguoNumericID.MatchString(cloned.DeviceID) || !hongguoNumericID.MatchString(cloned.InstallID) {
return
}
for name, cursor := range cloned.Feeds {
known := false
for _, genre := range hongguoAppGenres {
known = known || name == genre.key
}
if !known || cursor.Offset < 0 || cursor.Offset > 1_000_000 || len(cursor.SessionID) > 4096 {
delete(cloned.Feeds, name)
}
}
client.state = *cloned
}
func (downloader *Downloader) hongguoCatalogSnapshot() *hongguoCatalogState {
client := downloader.hongguoClient()
client.mu.Lock()
defer client.mu.Unlock()
return cloneHongguoCatalogState(&client.state)
}
func hongguoCatalogInitialized(state *hongguoCatalogState) bool {
if state != nil && state.Version == 1 {
for _, cursor := range state.Feeds {
if cursor.Initialized {
return true
}
}
}
return false
}
func hongguoCatalogHasMore(state *hongguoCatalogState) bool {
for _, genre := range hongguoAppGenres {
if state == nil || !state.Feeds[genre.key].Exhausted {
return true
}
}
return false
}
func (downloader *Downloader) hongguoAppRequest(ctx context.Context, method, path string, extra url.Values, payload any) (map[string]any, error) {
client := downloader.hongguoClient()
client.mu.Lock()
base, deviceID, installID := client.baseURL, client.state.DeviceID, client.state.InstallID
client.mu.Unlock()
query := url.Values{
"aid": {"8662"}, "app_name": {"novelread"}, "version_code": {"73532"}, "version_name": {"7.3.5.32"},
"manifest_version_code": {"73532"}, "update_version_code": {"73532"}, "channel": {"update_64"},
"device_platform": {"android"}, "os": {"android"}, "ssmix": {"a"}, "device_type": {"25053RT47C"},
"device_brand": {"Redmi"}, "language": {"zh"}, "os_api": {"36"}, "os_version": {"16"},
"resolution": {"1280*2772"}, "dpi": {"520"}, "ac": {"wifi"}, "device_id": {deviceID}, "iid": {installID},
}
for key, values := range extra {
query[key] = append([]string(nil), values...)
}
var body []byte
var err error
if payload != nil {
body, err = json.Marshal(payload)
if err != nil {
return nil, err
}
}
attempts := downloader.cfg.Retries
if attempts < 1 {
attempts = 1
}
if attempts > 3 {
attempts = 3
}
var lastErr error
for attempt := 0; attempt < attempts; attempt++ {
if attempt > 0 {
select {
case <-time.After(time.Duration(attempt) * time.Second):
case <-ctx.Done():
return nil, ctx.Err()
}
}
request, err := http.NewRequestWithContext(ctx, method, strings.TrimRight(base, "/")+path, bytes.NewReader(body))
if err != nil {
return nil, err
}
request.Header.Set("User-Agent", hongguoAppUserAgent)
request.Header.Set("Accept", "application/json")
request.Header.Set("X-XS-From-Web", "0")
request.Header.Set("Sdk-Version", "2")
comment, _ := ctx.Value(hongguoCommentKey{}).(bool)
var commentNonce hongguoCommentNonce
if comment {
commentNonce, err = newHongguoCommentNonce()
if err != nil {
return nil, errors.New("无法准备红果文字请求")
}
request.Header.Set("Comment-Source", "601")
request.Header.Set("Server-Channel", "1000")
}
if payload != nil {
request.Header.Set("Content-Type", "application/json; charset=utf-8")
}
response, err := downloader.doPreparedCatalogRequest(request, 20*time.Second, func(prepared *http.Request) {
now := time.Now()
query.Set("_rticket", strconv.FormatInt(now.UnixMilli(), 10))
prepared.URL.RawQuery = query.Encode()
if comment {
signHongguoCommentRequest(prepared, commentNonce, now)
} else {
signHongguoRequest(prepared, body, now)
}
})
if err != nil {
lastErr = err
var backoff *requestBackoff
if errors.As(err, &backoff) || ctx.Err() != nil {
return nil, err
}
continue
}
content, readErr := io.ReadAll(io.LimitReader(response.Body, providerMaxBodyBytes+1))
response.Body.Close()
if readErr != nil {
lastErr = readErr
continue
}
if response.StatusCode != http.StatusOK {
lastErr = fmt.Errorf("红果 App 接口 HTTP %d", response.StatusCode)
if response.StatusCode >= 400 && response.StatusCode < 500 {
return nil, lastErr
}
continue
}
if len(content) == 0 || len(content) > providerMaxBodyBytes {
return nil, errors.New("红果 App 接口未返回有效数据")
}
var result map[string]any
decoder := json.NewDecoder(bytes.NewReader(content))
decoder.UseNumber()
if err := decoder.Decode(&result); err != nil || result == nil {
return nil, errors.New("红果 App 接口返回格式异常")
}
code := firstNonEmpty(mapString(result, "code", "Code", "status_code"), mapString(nestedMap(result, "BaseResp"), "StatusCode"))
if code != "" && code != "0" {
return nil, fmt.Errorf("红果 App 接口暂不可用(%s)", truncate(code, 20))
}
return result, nil
}
return nil, lastErr
}