初始提交:短剧库源码(含转码优化 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
+174
View File
@@ -0,0 +1,174 @@
package app
import (
"context"
"errors"
"fmt"
"strconv"
"sync"
"time"
)
const rankingCacheTTL = 5 * time.Minute
type rankingBoard struct {
ID string `json:"id"`
Source string `json:"source"`
Name string `json:"name"`
Description string `json:"description"`
path string
upstreamKey string
}
var rankingBoards = []rankingBoard{
{ID: "hongguo-hot", Source: sourceHongguo, Name: "总热播榜", Description: "红果观看、互动等综合热度;每日更新。", path: "hot-drama", upstreamKey: "hongguo"},
{ID: "hongguo-real", Source: sourceHongguo, Name: "真人剧榜", Description: "红果真人剧热播榜;每日更新。", path: "hot-real-drama", upstreamKey: "real"},
{ID: "hongguo-comic", Source: sourceHongguo, Name: "漫剧榜", Description: "红果漫剧热播榜;每日更新。", path: "hot-comic-drama", upstreamKey: "comic"},
{ID: "hongguo-ai", Source: sourceHongguo, Name: "AI剧榜", Description: "红果 AI 剧热播榜;每日更新。", path: "hot-ai-drama", upstreamKey: "ai"},
{ID: "huangdou-all", Source: sourceHuangdou, Name: "总榜", Description: "黄豆短剧总榜,保留站点返回的顺序。", upstreamKey: "all"},
{ID: "huangdou-mogai", Source: sourceHuangdou, Name: "魔改榜", Description: "黄豆站点魔改榜。", upstreamKey: "mogai"},
{ID: "huangdou-search", Source: sourceHuangdou, Name: "搜索榜", Description: "黄豆站点搜索榜。", upstreamKey: "search"},
{ID: "huangdou-favorite", Source: sourceHuangdou, Name: "收藏榜", Description: "黄豆站点收藏榜。", upstreamKey: "favorite"},
{ID: "huangdou-finish", Source: sourceHuangdou, Name: "完结榜", Description: "黄豆站点完结榜。", upstreamKey: "finish"},
{ID: "huangguo-hot", Source: "huangguo", Name: "热播榜", Description: "黄果站点热播 TOP 20;每日更新。", path: "hot"},
{ID: "huangguo-recommend", Source: "huangguo", Name: "推荐榜", Description: "黄果站点推荐 TOP 20。", path: "recommend"},
{ID: "huangguo-potential", Source: "huangguo", Name: "潜力榜", Description: "黄果站点潜力 TOP 20。", path: "potential"},
}
type rankingItem struct {
Rank int `json:"rank"`
Drama Drama `json:"drama"`
Metric string `json:"metric,omitempty"`
}
type rankingPage struct {
BoardID string `json:"boardId"`
Page int `json:"page"`
Items []rankingItem `json:"items"`
HasMore bool `json:"hasMore"`
TotalPages int `json:"totalPages,omitempty"`
UpdatedText string `json:"updatedText,omitempty"`
FetchedAt time.Time `json:"fetchedAt"`
Stale bool `json:"stale,omitempty"`
Warning string `json:"warning,omitempty"`
}
type rankingCall struct {
done chan struct{}
page rankingPage
err error
}
type rankingCache struct {
mu sync.Mutex
pages map[string]rankingPage
pending map[string]*rankingCall
}
func findRankingBoard(id string) (rankingBoard, bool) {
for _, board := range rankingBoards {
if board.ID == id {
return board, true
}
}
return rankingBoard{}, false
}
func cloneRankingPage(page rankingPage) rankingPage {
page.Items = append([]rankingItem{}, page.Items...)
return page
}
func (d *Downloader) loadRankingPage(ctx context.Context, board rankingBoard, page int, refresh bool) (rankingPage, error) {
key := board.ID + ":" + strconv.Itoa(page)
cache := &d.rankings
cache.mu.Lock()
if cache.pages == nil {
cache.pages = map[string]rankingPage{}
cache.pending = map[string]*rankingCall{}
}
cached, exists := cache.pages[key]
if exists && !refresh && time.Since(cached.FetchedAt) < rankingCacheTTL {
cache.mu.Unlock()
return cloneRankingPage(cached), nil
}
if pending := cache.pending[key]; pending != nil {
cache.mu.Unlock()
select {
case <-ctx.Done():
return rankingPage{}, ctx.Err()
case <-pending.done:
if (errors.Is(pending.err, context.Canceled) || errors.Is(pending.err, context.DeadlineExceeded)) && ctx.Err() == nil {
return d.loadRankingPage(ctx, board, page, refresh)
}
return cloneRankingPage(pending.page), pending.err
}
}
pending := &rankingCall{done: make(chan struct{})}
cache.pending[key] = pending
cache.mu.Unlock()
result, err := d.fetchRankingPage(ctx, board, page)
cache.mu.Lock()
defer cache.mu.Unlock()
if err == nil {
result.BoardID, result.Page, result.FetchedAt = board.ID, page, time.Now()
if refresh && page == 1 {
for cachedKey, old := range cache.pages {
if old.BoardID == board.ID {
delete(cache.pages, cachedKey)
}
}
}
if len(cache.pages) >= 128 {
oldestKey, oldest := "", time.Now()
for cachedKey, old := range cache.pages {
if old.FetchedAt.Before(oldest) {
oldestKey, oldest = cachedKey, old.FetchedAt
}
}
delete(cache.pages, oldestKey)
}
cache.pages[key] = cloneRankingPage(result)
} else if exists && ctx.Err() == nil && time.Since(cached.FetchedAt) < 24*time.Hour {
result = cloneRankingPage(cached)
result.Stale = true
result.Warning = "站点暂不可用,显示上次取得的榜单,请稍后刷新。"
err = nil
}
pending.page, pending.err = cloneRankingPage(result), err
delete(cache.pending, key)
close(pending.done)
return result, err
}
func (d *Downloader) fetchRankingPage(ctx context.Context, board rankingBoard, page int) (rankingPage, error) {
switch board.Source {
case sourceHongguo:
pageURL := hongguoBaseURL + "/rank/" + board.path + "?page=" + strconv.Itoa(page)
body, err := d.fetchProviderText(ctx, pageURL, hongguoBaseURL+"/")
if err != nil {
return rankingPage{}, err
}
return parseHongguoRanking(body, board, page)
case sourceHuangdou:
var decoded any
err := newHuangdouAPIClient(d).call(ctx, "/drama/rank", map[string]any{"tab": board.upstreamKey, "page": strconv.Itoa(page)}, &decoded)
if err != nil {
return rankingPage{}, err
}
return parseHuangdouRanking(decoded, page)
case "huangguo":
if page != 1 {
return rankingPage{}, errors.New("该榜单只有一页")
}
pageURL := huangguoAIBaseURL + "/ranks/" + board.path + "/"
body, err := d.fetchProviderText(ctx, pageURL, huangguoAIBaseURL+"/")
if err != nil {
return rankingPage{}, err
}
return parseHuangguoRanking(body, board)
default:
return rankingPage{}, fmt.Errorf("不支持的榜单站源 %s", board.Source)
}
}