Files
djk/internal/app/provider_hongguo_detail.go

128 lines
4.2 KiB
Go

package app
import (
"context"
"errors"
"fmt"
"net/http"
"sort"
"strconv"
"time"
)
type hongguoDetailEntry struct {
Drama Drama
Chapters []Chapter
ExpiresAt time.Time
}
type hongguoDetailCall struct {
done chan struct{}
entry hongguoDetailEntry
err error
}
func (downloader *Downloader) hongguoAppDetail(ctx context.Context, seriesID string) (hongguoDetailEntry, error) {
if !hongguoNumericID.MatchString(seriesID) {
return hongguoDetailEntry{}, errors.New("红果剧集 ID 无效")
}
client := downloader.hongguoClient()
client.mu.Lock()
if cached, found := client.details[seriesID]; found && time.Now().Before(cached.ExpiresAt) {
client.mu.Unlock()
return cached, nil
}
if pending := client.pending[seriesID]; pending != nil {
client.mu.Unlock()
select {
case <-ctx.Done():
return hongguoDetailEntry{}, ctx.Err()
case <-pending.done:
if (errors.Is(pending.err, context.Canceled) || errors.Is(pending.err, context.DeadlineExceeded)) && ctx.Err() == nil {
return downloader.hongguoAppDetail(ctx, seriesID)
}
return pending.entry, pending.err
}
}
pending := &hongguoDetailCall{done: make(chan struct{})}
client.pending[seriesID] = pending
client.mu.Unlock()
result, err := downloader.hongguoAppRequest(ctx, http.MethodPost, "/novel/player/video_detail/v1/", nil, map[string]any{"series_id": seriesID})
var entry hongguoDetailEntry
if err == nil {
entry, err = parseHongguoAppDetail(result, seriesID)
}
client.mu.Lock()
if err == nil {
if len(client.details) >= 128 {
client.details = map[string]hongguoDetailEntry{}
}
entry.ExpiresAt = time.Now().Add(5 * time.Minute)
client.details[seriesID] = entry
}
pending.entry, pending.err = entry, err
delete(client.pending, seriesID)
close(pending.done)
client.mu.Unlock()
return entry, err
}
func parseHongguoAppDetail(result map[string]any, seriesID string) (hongguoDetailEntry, error) {
detail := nestedMap(result, "data", "video_data")
returnedID := mapString(detail, "series_id_str", "series_id")
if returnedID != seriesID {
return hongguoDetailEntry{}, errors.New("红果 App 未返回所请求的剧集")
}
entry := hongguoDetailEntry{Drama: hongguoDramaFromAny(detail, "短剧")}
seen := map[string]bool{}
episodes := map[int]bool{}
for _, row := range anyList(detail["video_list"]) {
video, _ := row.(map[string]any)
videoID := mapString(video, "vid")
index, err := strconv.Atoi(mapString(video, "vid_index"))
if err != nil || index < 1 || !hongguoNumericID.MatchString(videoID) {
return entry, errors.New("红果 App 分集编号或视频 ID 无效")
}
if identifier := mapString(video, "series_id"); identifier != "" && identifier != seriesID {
return entry, errors.New("红果 App 返回了其他剧集的分集")
}
if seen[videoID] || episodes[index] {
return entry, errors.New("红果 App 返回了重复分集")
}
seen[videoID], episodes[index] = true, true
entry.Chapters = append(entry.Chapters, Chapter{
ID: providerChapterID(sourceHongguo, seriesID, videoID), Source: sourceHongguo,
Title: fmt.Sprintf("第%d集", index), VideoURL: "hongguo-cenc://" + videoID, CurrentEpisode: rawEpisode(index),
})
}
sort.Slice(entry.Chapters, func(left, right int) bool {
leftIndex, _ := strconv.Atoi(entry.Chapters[left].EpisodeString(left + 1))
rightIndex, _ := strconv.Atoi(entry.Chapters[right].EpisodeString(right + 1))
return leftIndex < rightIndex
})
total, _ := strconv.Atoi(mapString(detail, "episode_cnt"))
if len(entry.Chapters) == 0 || total > len(entry.Chapters) {
return entry, errors.New("红果 App 未返回完整分集,已尝试其他详情入口")
}
for index, chapter := range entry.Chapters {
if chapter.EpisodeString(index+1) != strconv.Itoa(index+1) {
return entry, errors.New("红果 App 分集列表不连续")
}
}
return entry, nil
}
func (downloader *Downloader) hongguoCachedDrama(drama Drama) Drama {
source, seriesID, valid := splitProviderDramaID(drama.ID)
if !valid || source != sourceHongguo {
return drama
}
client := downloader.hongguoClient()
client.mu.Lock()
defer client.mu.Unlock()
if cached, found := client.details[seriesID]; found && time.Now().Before(cached.ExpiresAt) {
return mergeDramaMetadata(cached.Drama, drama)
}
return drama
}