169 lines
5.1 KiB
Go
169 lines
5.1 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
type providerMedia struct {
|
|
URL string
|
|
Referer string
|
|
Duration time.Duration
|
|
Playlist string
|
|
HLSKey []byte
|
|
CENCKey []byte
|
|
Quality int
|
|
Variants []providerMedia
|
|
}
|
|
|
|
func (d *Downloader) providerBaseURL(source string) string {
|
|
source = canonicalProviderSource(source)
|
|
var configured, fallback string
|
|
switch source {
|
|
case sourceHuangguoAI:
|
|
configured, fallback = d.cfg.HuangguoAIURL, huangguoAIBaseURL
|
|
case sourceHuangguoVideo:
|
|
configured, fallback = d.cfg.HuangguoVideoURL, huangguoVideoBaseURL
|
|
case sourceHuangdou:
|
|
configured, fallback = d.cfg.HuangdouURL, huangdouBaseURL
|
|
case sourceHongguo:
|
|
configured, fallback = d.cfg.HongguoURL, hongguoBaseURL
|
|
default:
|
|
fallback = "https://d2pypzndaqisk.cloudfront.net"
|
|
}
|
|
return strings.TrimRight(firstNonEmpty(configured, fallback), "/")
|
|
}
|
|
|
|
func providerSourceForURL(raw string) string {
|
|
parsed, err := url.Parse(raw)
|
|
if err != nil {
|
|
return ""
|
|
}
|
|
host := strings.ToLower(parsed.Hostname())
|
|
switch {
|
|
case host == "huangguoai.com" || strings.HasSuffix(host, ".ediayikma.cc") || strings.HasSuffix(host, ".agdkczeyx.cc"):
|
|
return sourceHuangguoAI
|
|
case host == "huangguo.video":
|
|
return sourceHuangguoVideo
|
|
case host == "tideember.cc" || host == "xqjurgek.top":
|
|
return sourceHuangdou
|
|
case host == "hongguoduanju.com" || host == "www.hongguoduanju.com":
|
|
return sourceHongguo
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
func (d *Downloader) providerURLCandidates(raw string) []string {
|
|
source := providerSourceForURL(raw)
|
|
if source == "" {
|
|
return []string{raw}
|
|
}
|
|
parsed, _ := url.Parse(raw)
|
|
d.providerMu.Lock()
|
|
preferred := d.providerHosts[source]
|
|
d.providerMu.Unlock()
|
|
var candidates []string
|
|
seen := map[string]bool{}
|
|
add := func(candidate string) {
|
|
if candidate != "" && !seen[candidate] {
|
|
seen[candidate] = true
|
|
candidates = append(candidates, candidate)
|
|
}
|
|
}
|
|
add(rehostProviderURL(parsed, preferred))
|
|
configured := d.providerBaseURL(source)
|
|
add(rehostProviderURL(parsed, configured))
|
|
if providerSourceForURL(configured) == "" {
|
|
return candidates
|
|
}
|
|
for _, candidate := range providerURLCandidates(raw) {
|
|
add(candidate)
|
|
}
|
|
return candidates
|
|
}
|
|
|
|
func (d *Downloader) resolveProviderMedia(ctx context.Context, task Task) (providerMedia, error) {
|
|
chapter := task.Chapter
|
|
chapter.Source = canonicalProviderSource(chapter.Source)
|
|
if chapter.Source == "" {
|
|
chapter.Source = sourceFromDramaID(task.DramaID)
|
|
}
|
|
if strings.HasPrefix(chapter.VideoURL, "hongguo-cenc://") {
|
|
return d.resolveHongguoMedia(ctx, task)
|
|
}
|
|
if chapter.PageURL == "" && (chapter.Source == sourceHuangguoAI || chapter.Source == sourceHuangguoVideo) {
|
|
if source, sourceID, valid := splitProviderDramaID(task.DramaID); valid {
|
|
_, chapters, err := d.GetHuangguoChapters(ctx, source, sourceID)
|
|
if err != nil {
|
|
return providerMedia{}, fmt.Errorf("刷新旧任务播放地址失败: %w", err)
|
|
}
|
|
matched := false
|
|
for _, fresh := range chapters {
|
|
if fresh.ID == chapter.ID {
|
|
chapter = fresh
|
|
matched = true
|
|
break
|
|
}
|
|
}
|
|
if !matched {
|
|
return providerMedia{}, fmt.Errorf("原章节已变化,请更新该合集后重新下载")
|
|
}
|
|
}
|
|
}
|
|
media := providerMedia{URL: chapter.VideoURL, Referer: firstNonEmpty(chapter.Referer, d.providerBaseURL(chapter.Source)+"/")}
|
|
if chapter.Source == sourceHuangdou {
|
|
_, sourceID, valid := splitProviderDramaID(task.DramaID)
|
|
if valid {
|
|
sequence, err := strconv.Atoi(chapter.EpisodeString(task.Index))
|
|
if err != nil || sequence < 1 {
|
|
return providerMedia{}, fmt.Errorf("黄豆集数无效")
|
|
}
|
|
client := newHuangdouAPIClient(d)
|
|
media, err = d.resolveHuangdouPlayback(ctx, client, sourceID, sequence)
|
|
if err != nil {
|
|
return providerMedia{}, err
|
|
}
|
|
media.Referer = client.host + "/home"
|
|
}
|
|
}
|
|
if chapter.PageURL != "" && (chapter.Source == sourceHuangguoAI || chapter.Source == sourceHuangguoVideo) {
|
|
body, err := d.fetchProviderText(ctx, chapter.PageURL, media.Referer)
|
|
if err != nil {
|
|
return providerMedia{}, err
|
|
}
|
|
if chapter.Source == sourceHuangguoAI {
|
|
media.URL = parseAIVideoURL(body, chapter.PageURL)
|
|
} else {
|
|
media.URL = parseDataHLS(body, chapter.PageURL)
|
|
}
|
|
d.providerMu.Lock()
|
|
if preferred := d.providerHosts[chapter.Source]; preferred != "" {
|
|
media.Referer = preferred + "/"
|
|
}
|
|
d.providerMu.Unlock()
|
|
}
|
|
if !isProviderHTTPMediaURL(media.URL) {
|
|
return providerMedia{}, fmt.Errorf("%s 未返回有效播放地址,请刷新章节或确认站点访问权限", chapter.Source)
|
|
}
|
|
parsed, _ := url.Parse(media.URL)
|
|
if strings.HasSuffix(strings.ToLower(parsed.Path), ".m3u8") {
|
|
playlist, err := d.fetchProviderText(ctx, media.URL, media.Referer)
|
|
if err != nil {
|
|
return providerMedia{}, fmt.Errorf("获取播放列表失败: %w", err)
|
|
}
|
|
if !strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(playlist, "\ufeff")), "#EXTM3U") {
|
|
return providerMedia{}, fmt.Errorf("站点未返回有效 M3U8,可能需要登录或链接已失效")
|
|
}
|
|
if duration := m3u8Duration(playlist); duration > 0 {
|
|
media.Duration = duration
|
|
}
|
|
media.Playlist = playlist
|
|
}
|
|
return media, nil
|
|
}
|