181 lines
5.2 KiB
Go
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 = ©
|
|
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
|
|
}
|