Files
ggjuku/internal/app/network_diagnostics.go
T
2026-09-30 10:20:58 +08:00

181 lines
5.2 KiB
Go

package app
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"sync"
"time"
)
type networkCheckResult struct {
Name string `json:"name"`
Status int `json:"status,omitempty"`
Detail string `json:"detail,omitempty"`
Error string `json:"error,omitempty"`
}
func (a *UIApp) networkCheckResults(ctx context.Context) []networkCheckResult {
targets := []struct{ name, endpoint string }{
{"红果入口", a.downloader.providerBaseURL(sourceHongguo) + "/"},
}
var mediaTask *Task
a.mu.Lock()
concurrency := a.cfg.RequestConcurrency
for _, drama := range a.dramas {
if dramaProvider(drama) == sourceHongguo {
if endpoint, valid := buildImageURL(bestDramaCover(drama)); valid {
targets = append(targets, struct{ name, endpoint string }{"红果封面 CDN", endpoint})
break
}
}
}
for index := len(a.taskOrder) - 1; index >= 0; index-- {
task := a.tasks[a.taskOrder[index]]
if task == nil || isChapterPlaceholderTask(task) {
continue
}
source := sourceFromDramaID(task.DramaID)
if source == sourceHongguo {
copy := task.Source
mediaTask = &copy
if task.Status == uiStatusFailed {
break
}
}
}
a.mu.Unlock()
if concurrency < 1 {
concurrency = 1
}
if concurrency > 2 {
concurrency = 2
}
results := make([]networkCheckResult, len(targets)+1)
jobs := make(chan int)
var workers sync.WaitGroup
for worker := 0; worker < concurrency; worker++ {
workers.Add(1)
go func() {
defer workers.Done()
for index := range jobs {
if index == len(targets) {
results[index] = a.checkHongguoMedia(ctx, mediaTask)
continue
}
target := targets[index]
probeCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
status, err := a.checkNetworkResource(probeCtx, http.MethodGet, target.endpoint, imageReferer(target.endpoint), false)
cancel()
results[index] = networkCheckResult{Name: target.name, Status: status, Error: a.redactError(err)}
}
}()
}
for index := range results {
jobs <- index
}
close(jobs)
workers.Wait()
return results
}
func (a *UIApp) checkNetworkResource(ctx context.Context, method, endpoint, referer string, key bool) (int, error) {
upstream, err := http.NewRequestWithContext(ctx, method, endpoint, nil)
if err != nil {
return 0, err
}
upstream.Header.Set("User-Agent", userAgent)
upstream.Header.Set("Referer", referer)
response, err := a.downloader.client.Do(upstream)
if err != nil {
return 0, publicError(err)
}
defer response.Body.Close()
if response.StatusCode < 200 || response.StatusCode >= 300 {
return response.StatusCode, fmt.Errorf("%s HTTP %d", upstream.URL.Hostname(), response.StatusCode)
}
if key {
body, err := io.ReadAll(io.LimitReader(response.Body, 17))
if err != nil {
return response.StatusCode, err
}
if len(body) != 16 {
return response.StatusCode, fmt.Errorf("密钥响应不是有效的 16 字节 AES-128 密钥")
}
} else {
if _, err := io.Copy(io.Discard, io.LimitReader(response.Body, 32768)); err != nil {
return response.StatusCode, err
}
}
return response.StatusCode, nil
}
func (a *UIApp) checkHongguoMedia(ctx context.Context, task *Task) networkCheckResult {
result := networkCheckResult{Name: "红果实际媒体"}
if task == nil {
result.Detail = "暂无红果分集任务,入口可达不代表媒体可下载"
return result
}
ctx, cancel := context.WithTimeout(ctx, 40*time.Second)
defer cancel()
media, err := a.downloader.resolveProviderMedia(ctx, *task)
if err != nil {
result.Error = "播放地址解析失败: " + a.redactError(err)
return result
}
for depth := 0; depth < 3; depth++ {
variant := selectBestM3U8Variant(media.Playlist, media.URL)
if variant == "" {
break
}
media.Playlist, media.URL, err = a.downloader.fetchMediaPlaylist(ctx, variant, media.Referer)
if err != nil {
result.Error = "播放列表失败: " + a.redactError(err)
return result
}
}
base, _ := url.Parse(media.URL)
segment := media.URL
keyChecked := false
for _, line := range strings.Split(media.Playlist, "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "#EXT-X-KEY:") && !strings.Contains(line, "METHOD=NONE") {
match := hlsURIAttribute.FindStringSubmatch(line)
if len(match) == 2 && !keyChecked {
reference, parseErr := url.Parse(match[1])
if parseErr == nil {
result.Status, err = a.checkNetworkResource(ctx, http.MethodGet, base.ResolveReference(reference).String(), media.Referer, true)
} else {
err = parseErr
}
if err != nil {
result.Error = "视频密钥失败: " + a.redactError(err)
return result
}
keyChecked = true
}
} else if line != "" && !strings.HasPrefix(line, "#") {
reference, parseErr := url.Parse(line)
if parseErr != nil {
result.Error = a.redactError(parseErr)
return result
}
segment = base.ResolveReference(reference).String()
break
}
}
result.Status, err = a.checkNetworkResource(ctx, http.MethodHead, segment, media.Referer, false)
if err != nil {
result.Error = "媒体分片检测失败: " + a.redactError(err)
return result
}
result.Detail = "播放地址、首个媒体资源可达(不等于整集下载验证)"
if keyChecked {
result.Detail = "播放列表、16 字节密钥、首个分片均可达(不等于整集下载验证)"
}
return result
}