初始提交:短剧库源码(含转码优化 ultrafast)

This commit is contained in:
yvan
2026-09-17 19:09:08 +08:00
commit 07dc0f1059
247 changed files with 43242 additions and 0 deletions
+261
View File
@@ -0,0 +1,261 @@
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
}