初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user