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 }