Files
djk/internal/app/provider_sort_details.go

262 lines
9.0 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package app
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"net/url"
"regexp"
"strconv"
"strings"
"time"
)
var sortMetadataMetric = regexp.MustCompile(`(?i)^\d+(?:\.\d+)?(?:亿|万|千|w|k|m|b)?\+?(?:次播放|次观看|人看过|热度|播放|观看|次)?\+?$`)
func hasSortMetric(value string) bool {
return sortMetadataMetric.MatchString(strings.NewReplacer(",", "", ",", "", " ", "", "\t", "").Replace(value))
}
func hasCompleteSortMetadata(drama Drama) bool {
if providerReleaseDate(drama.OnlineDate) == "" || !hasSortMetric(drama.Views) {
return false
}
switch dramaProvider(drama) {
case sourceHongguo, sourceHuangdou:
return hasSortMetric(drama.Heat)
case sourceHuangguoAI, sourceHuangguoVideo:
return true
}
return false
}
func providerReleaseDate(value string) string {
value = strings.TrimSpace(value)
if stamp, err := time.Parse(time.RFC3339Nano, value); err == nil {
value = stamp.In(providerChinaTime).Format("2006-01-02")
}
date := normalizeDate(value)
if parsed, err := time.Parse("2006-01-02", date); err != nil || parsed.Year() < 2000 || parsed.Year() > 2100 {
return ""
}
return date
}
func providerTimestampDate(value string) string {
stamp, err := strconv.ParseInt(strings.TrimSpace(value), 10, 64)
if err != nil || stamp <= 0 {
return ""
}
if stamp > 100_000_000_000 {
stamp /= 1000
}
date := time.Unix(stamp, 0).In(providerChinaTime)
if date.Year() < 2000 || date.Year() > 2100 {
return ""
}
return date.Format("2006-01-02")
}
func (d *Downloader) fetchDramaSortMetadata(ctx context.Context, drama Drama) (Drama, error) {
source, id, ok := splitProviderDramaID(drama.ID)
patch := Drama{ID: drama.ID, Source: source, SourceID: id}
if !ok {
return patch, errors.New("无效的剧集 ID")
}
needCover := needsHongguoCoverAddress(drama)
if hasCompleteSortMetadata(drama) && !huangguoNeedsContentMetadata(drama) && !needCover && !needsHuangdouVIPMetadata(drama) {
return patch, nil
}
switch source {
case sourceHongguo:
if !hongguoNumericID.MatchString(id) {
return patch, errors.New("无效的红果剧集 ID")
}
var failures []error
if providerReleaseDate(drama.OnlineDate) == "" {
body, err := d.fetchProviderText(ctx, hongguoBaseURL+"/detail?series_id="+url.QueryEscape(id), hongguoBaseURL+"/")
if err == nil {
var web Drama
web, err = parseHongguoSortDetail(body, id)
patch = mergeDramaMetadata(web, patch)
}
if err != nil {
failures = append(failures, err)
}
}
needMetrics := !hasSortMetric(drama.Heat) || !hasSortMetric(drama.Views)
if !needMetrics && (!needCover || bestDramaCover(patch) != "") {
return patch, errors.Join(failures...)
}
result, appErr := d.hongguoAppRequest(ctx, http.MethodPost, "/novel/player/video_detail/v1/", nil, map[string]any{"series_id": id})
if appErr == nil {
row := nestedMap(result, "data", "video_data")
if mapString(row, "series_id_str", "series_id") != id {
appErr = errors.New("红果详情返回了其他剧集")
} else {
patch.Title = mapString(row, "series_title", "series_name")
if needMetrics {
patch.Heat = hongguoHeat(row)
patch.Views = normalizeViews(mapString(row, "series_play_cnt", "play_cnt"))
}
if needCover {
if cover := hongguoCoverAddress(mapString(row, "series_cover", "cover")); cover != "" {
patch.Cover, patch.CoverURL = cover, cover
}
}
}
}
if appErr != nil {
failures = append(failures, appErr)
}
return patch, errors.Join(failures...)
case sourceHuangdou:
if !rankingSourceID.MatchString(id) {
return patch, errors.New("无效的黄豆剧集 ID")
}
row, err := d.huangdouDetail(ctx, id)
if err != nil {
return patch, err
}
patch.VIP = huangdouVIPFlag(row)
patch.Title = mapString(row, "name", "title")
patch.OnlineDate = providerReleaseDate(mapString(row, "issue_date"))
patch.Views = normalizeViews(mapString(row, "click"))
patch.Heat = mapString(row, "hot_rate")
if !hasSortMetric(patch.Heat) && !hasSortMetric(drama.Heat) {
keyword := firstNonEmpty(patch.Title, drama.DisplayTitle())
var decoded any
if err := newHuangdouAPIClient(d).call(ctx, "/drama/list", map[string]any{"keywords": keyword, "page": "1", "page_size": "50"}, &decoded); err != nil {
return patch, err
}
matched := false
for _, item := range huangdouList(decoded) {
if strings.TrimPrefix(mapString(item, "id", "drama_id"), "rp_") == id {
matched = true
patch.Heat = mapString(item, "hot_rate")
break
}
}
if !matched {
return patch, errors.New("黄豆热度查询未返回所请求剧集,可稍后重试")
}
}
return patch, nil
case sourceHuangguoAI, sourceHuangguoVideo:
pageURL, err := huangguoSortDetailURL(source, id)
if err != nil {
return patch, err
}
body, err := d.fetchProviderText(ctx, pageURL, providerRefererForURL(pageURL, ""))
if err != nil {
return patch, err
}
return parseHuangguoSortDetail(body, pageURL, patch)
}
return patch, errors.New("该站源暂无资料补齐接口")
}
func huangguoSortDetailURL(source, id string) (string, error) {
if source == sourceHuangguoVideo {
parts := strings.Split(id, "/")
if len(parts) == 1 {
parts = []string{"series", id}
}
if len(parts) != 2 || (parts[0] != "series" && parts[0] != "video") || !rankingSourceID.MatchString(parts[1]) {
return "", errors.New("无效的黄果剧集 ID")
}
return huangguoVideoBaseURL + "/" + parts[0] + "/" + url.PathEscape(parts[1]), nil
}
if source != sourceHuangguoAI || !rankingSourceID.MatchString(id) {
return "", errors.New("无效的黄果剧集 ID")
}
return huangguoAIBaseURL + "/detail/" + url.PathEscape(id) + "/", nil
}
func parseHongguoSortDetail(body, id string) (Drama, error) {
row := nestedMap(routerLoaderMap(parseRouterData(body), "detail_page", "detail_"), "seriesDetail")
if mapString(row, "series_id", "series_id_str") != id || mapString(row, "series_name", "series_title") == "" {
return Drama{}, errors.New("红果网页详情与请求剧集不符")
}
patch := hongguoDramaFromAny(row, "")
patch.Title = mapString(row, "series_name", "series_title")
patch.Name = patch.Title
patch.OnlineDate = providerTimestampDate(mapString(row, "first_visible_time"))
if cover := hongguoCoverAddress(mapString(row, "series_cover", "cover")); cover != "" {
patch.Cover, patch.CoverURL = cover, cover
}
return patch, nil
}
func parseHuangguoSortDetail(body, pageURL string, patch Drama) (Drama, error) {
type entry struct {
Type string `json:"@type"`
ID string `json:"@id"`
URL string `json:"url"`
Name string `json:"name"`
Published string `json:"datePublished"`
Uploaded string `json:"uploadDate"`
Image any `json:"image"`
Thumbnail any `json:"thumbnailUrl"`
}
expected, _ := url.Parse(pageURL)
matched := false
for _, block := range rankingJSONLD.FindAllStringSubmatch(body, -1) {
var graph struct {
Graph []entry `json:"@graph"`
entry
}
if json.Unmarshal([]byte(block[1]), &graph) != nil {
continue
}
for _, row := range append(graph.Graph, graph.entry) {
if row.Type != "WebPage" && row.Type != "VideoObject" && row.Type != "TVSeries" && row.Type != "Movie" {
continue
}
actual, err := url.Parse(firstNonEmpty(row.URL, row.ID))
if err != nil || actual.Host != "" && !strings.EqualFold(actual.Host, expected.Host) && providerSourceForURL(actual.String()) != patch.Source || strings.TrimRight(actual.Path, "/") != strings.TrimRight(expected.Path, "/") || row.Name == "" || patch.Source == sourceHuangguoAI && huangguoTitleNeedsRepair(row.Name, patch.SourceID) {
continue
}
matched = true
patch.Title = row.Name
if address := firstNonEmpty(providerCoverAddress(row.Image, pageURL), providerCoverAddress(row.Thumbnail, pageURL)); address != "" {
patch.Cover, patch.CoverURL = address, address
}
if date := providerReleaseDate(firstNonEmpty(row.Uploaded, row.Published)); date != "" && (patch.OnlineDate == "" || row.Type == "VideoObject") {
patch.OnlineDate = date
}
}
}
if !matched {
return patch, fmt.Errorf("黄果详情没有返回所请求剧集的元数据")
}
if bestDramaCover(patch) == "" {
for _, tag := range coverMetaTag.FindAllString(body, -1) {
if strings.ToLower(extractAttr(tag, "property", "name")) == "og:image" {
if address := providerCoverAddress(extractAttr(tag, "content"), pageURL); address != "" {
patch.Cover, patch.CoverURL = address, address
break
}
}
}
}
markup := huangguoNonContent.ReplaceAllString(body, "")
metaBlock := huangguoClassBlock(markup, "hg-web-detail__meta")
meta := cleanText(metaBlock)
patch.Views = normalizeViews(firstMatchText(reViewsText, meta))
if patch.OnlineDate == "" && strings.Contains(meta, "上线") {
patch.OnlineDate = providerReleaseDate(firstMatchText(reDateText, meta))
}
if patch.Source == sourceHuangguoAI {
episode := firstNonEmpty(extractAttr(metaBlock, "data-ep-base"), meta)
if count := episodeIndex(episode, 0); count > 0 {
patch.TotalEpisode, patch.EpisodeCount = count, count
}
}
return patch, nil
}