175 lines
6.2 KiB
Go
175 lines
6.2 KiB
Go
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)
|
|
}
|
|
}
|