初始提交:短剧库源码(含转码优化 ultrafast)
This commit is contained in:
@@ -0,0 +1,168 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user