Files
djk/internal/app/provider_rankings_parse.go

176 lines
7.2 KiB
Go

package app
import (
"encoding/json"
"errors"
"fmt"
"net/url"
"regexp"
"strconv"
"strings"
)
var rankingScriptTags = regexp.MustCompile(`(?is)<script\b[^>]*>`)
var rankingJSONLD = regexp.MustCompile(`(?is)<script\b[^>]*type=["']application/ld\+json["'][^>]*>(.*?)</script>`)
var rankingSourceID = regexp.MustCompile(`^[a-zA-Z0-9_-]{1,100}$`)
type hongguoRankingContent struct {
Success bool `json:"isSuccess"`
Rows []struct {
ID string `json:"id"`
SeriesID string `json:"seriesId"`
Rank int `json:"rank"`
Title string `json:"title"`
Heat string `json:"heatText"`
Score string `json:"scoreText"`
Tags []string `json:"tags"`
Description string `json:"description"`
EpisodeIDs []string `json:"episodeVids"`
Cover any `json:"cover"`
} `json:"rankList"`
Pagination struct {
Page int `json:"pageNum"`
TotalPages int `json:"totalPages"`
} `json:"pagination"`
}
func parseHongguoRanking(body string, board rankingBoard, page int) (rankingPage, error) {
failure := errors.New("红果榜单格式或分页已变化,请稍后重试")
loaderKey := "rank_" + board.path + "/page"
loader := nestedMap(parseRouterData(body), "loaderData", loaderKey)
if mapString(loader, "rankKey") != board.upstreamKey || mapString(loader, "pageNum") != strconv.Itoa(page) {
return rankingPage{}, failure
}
var content hongguoRankingContent
if inline := nestedMap(loader, "content"); len(inline) > 0 {
raw, err := json.Marshal(inline)
if err != nil || json.Unmarshal(raw, &content) != nil {
return rankingPage{}, failure
}
} else if merged, found := parseHongguoMergeLoader(body, loaderKey); found {
content = merged
} else {
for _, tag := range rankingScriptTags.FindAllString(body, -1) {
if extractAttr(tag, "data-fn-name") != "r" || extractAttr(tag, "data-script-src") != "modern-run-router-data-fn" {
continue
}
var args []json.RawMessage
if json.Unmarshal([]byte(extractAttr(tag, "data-fn-args")), &args) != nil || len(args) != 3 {
continue
}
var route, field string
if json.Unmarshal(args[0], &route) != nil || json.Unmarshal(args[1], &field) != nil || route != loaderKey || field != "content" {
continue
}
if json.Unmarshal(args[2], &content) != nil {
return rankingPage{}, failure
}
break
}
}
if !content.Success || content.Rows == nil || content.Pagination.Page != page || content.Pagination.TotalPages < page || content.Pagination.TotalPages > 500 {
return rankingPage{}, failure
}
result := rankingPage{Items: make([]rankingItem, 0, len(content.Rows)), TotalPages: content.Pagination.TotalPages, HasMore: page < content.Pagination.TotalPages, UpdatedText: mapString(loader, "updatedText")}
seen := map[string]bool{}
previous := (page - 1) * 20
for _, row := range content.Rows {
id := firstNonEmpty(row.SeriesID, row.ID)
if !hongguoNumericID.MatchString(id) || row.ID != "" && row.SeriesID != "" && row.ID != row.SeriesID || strings.TrimSpace(row.Title) == "" || row.Rank <= previous || row.Rank > page*20 || seen[id] {
return rankingPage{}, failure
}
seen[id], previous = true, row.Rank
drama := Drama{ID: providerDramaID(sourceHongguo, id), Source: sourceHongguo, SourceID: id, Title: row.Title, Name: row.Title, Desc: row.Description, Intro: row.Description, Tags: row.Tags, Heat: row.Heat, Score: strings.TrimPrefix(row.Score, "评分"), ChannelName: "红果"}
if cover := hongguoCoverAddress(coverPathFromAny(row.Cover)); cover != "" {
drama.Cover, drama.CoverURL = cover, cover
}
if len(row.Tags) > 0 {
drama.CategoryName = row.Tags[0]
}
if len(row.EpisodeIDs) > 0 {
drama.TotalEpisode = len(row.EpisodeIDs)
}
result.Items = append(result.Items, rankingItem{Rank: row.Rank, Drama: drama, Metric: row.Heat})
}
if len(result.Items) == 0 && result.HasMore {
return rankingPage{}, failure
}
return result, nil
}
func parseHuangdouRanking(decoded any, page int) (rankingPage, error) {
data := huangdouDataMap(decoded)
rows, valid := data["list"].([]any)
if !valid {
return rankingPage{}, errors.New("黄豆未返回有效榜单")
}
if len(rows) > 20 {
return rankingPage{}, errors.New("黄豆榜单分页格式已变化")
}
result := rankingPage{Items: make([]rankingItem, 0, len(rows)), HasMore: len(rows) == 20}
seen := map[string]bool{}
for index, value := range rows {
row, valid := value.(map[string]any)
id := strings.TrimPrefix(firstNonEmpty(mapString(row, "id"), mapString(row, "drama_id")), "rp_")
title := mapString(row, "name", "title", "t")
if !valid || !rankingSourceID.MatchString(id) || strings.TrimSpace(title) == "" || seen[id] {
return rankingPage{}, errors.New("黄豆榜单包含无效或重复条目")
}
seen[id] = true
remark := firstNonEmpty(mapString(row, "update_label"), mapString(row, "corner"))
drama := Drama{VIP: huangdouVIPFlag(row), ID: providerDramaID(sourceHuangdou, id), Source: sourceHuangdou, SourceID: id, Title: title, Name: title, ChannelName: "黄豆", CategoryName: mapString(row, "category"), TotalEpisode: mapString(row, "episode_count"), Remark: remark, ReleaseStatus: releaseStatusFromRemark(remark), Heat: mapString(row, "hot_rate"), Views: normalizeViews(mapString(row, "click")), OnlineDate: providerReleaseDate(mapString(row, "issue_date"))}
metric := ""
if drama.Heat != "" {
metric = drama.Heat + "热度"
}
result.Items = append(result.Items, rankingItem{Rank: (page-1)*20 + index + 1, Drama: drama, Metric: metric})
}
return result, nil
}
func parseHuangguoRanking(body string, board rankingBoard) (rankingPage, error) {
failure := errors.New("黄果未返回有效榜单,可能是页面结构变化")
for _, match := range rankingJSONLD.FindAllStringSubmatch(body, -1) {
var schema struct {
Graph []struct {
Type string `json:"@type"`
ID string `json:"@id"`
Items []struct {
Position int `json:"position"`
Name string `json:"name"`
URL string `json:"url"`
} `json:"itemListElement"`
} `json:"@graph"`
}
if json.Unmarshal([]byte(match[1]), &schema) != nil {
continue
}
for _, graph := range schema.Graph {
if graph.Type != "ItemList" || !strings.HasSuffix(graph.ID, "/ranks/"+board.path+"/#itemlist") {
continue
}
if len(graph.Items) == 0 || len(graph.Items) > 100 {
return rankingPage{}, failure
}
result := rankingPage{Items: make([]rankingItem, 0, len(graph.Items)), TotalPages: 1}
seen, previous := map[string]bool{}, 0
for _, item := range graph.Items {
parsed, err := url.Parse(item.URL)
if err != nil || !strings.HasPrefix(parsed.Path, "/detail/") {
return rankingPage{}, failure
}
id := strings.TrimSuffix(strings.TrimPrefix(parsed.Path, "/detail/"), "/")
if !rankingSourceID.MatchString(id) || strings.TrimSpace(item.Name) == "" || item.Position <= previous || seen[id] {
return rankingPage{}, failure
}
seen[id], previous = true, item.Position
drama := Drama{ID: providerDramaID(sourceHuangguoAI, id), Source: sourceHuangguoAI, SourceID: id, Title: item.Name, Name: item.Name}
result.Items = append(result.Items, rankingItem{Rank: item.Position, Drama: drama})
}
return result, nil
}
}
return rankingPage{}, fmt.Errorf("%s: %w", board.Name, failure)
}