package app import ( "context" "encoding/json" "errors" "net/http" "net/url" "regexp" "sort" "strconv" "strings" ) type playbackQualityKey struct{} type playbackQualityOption struct { Value int `json:"value"` Label string `json:"label"` } var playbackHLSResolution = regexp.MustCompile(`(?:^|,)RESOLUTION=(\d+)x(\d+)(?:,|$)`) func parsePlaybackQuality(value string) (int, error) { if value == "" || value == "auto" { return 0, nil } quality, err := strconv.Atoi(value) if err != nil || quality < 0 || quality > 4320 { return 0, errors.New("清晰度选项无效") } return quality, nil } func playbackQualityOptions(media providerMedia) []playbackQualityOption { options := make([]playbackQualityOption, 0, len(media.Variants)) seen := make(map[int]bool) for _, variant := range media.Variants { if variant.Quality <= 0 || variant.Quality > 4320 || seen[variant.Quality] { continue } seen[variant.Quality] = true options = append(options, playbackQualityOption{Value: variant.Quality, Label: strconv.Itoa(variant.Quality) + "p"}) } sort.Slice(options, func(i, j int) bool { return options[i].Value > options[j].Value }) return options } func setPlaybackQualityHeaders(header http.Header, media providerMedia) { options, _ := json.Marshal(playbackQualityOptions(media)) header.Set("X-Playback-Qualities", string(options)) header.Set("X-Playback-Quality", strconv.Itoa(media.Quality)) } func (downloader *Downloader) selectPlaybackQuality(ctx context.Context, media providerMedia) (providerMedia, error) { requested, _ := ctx.Value(playbackQualityKey{}).(int) if requested > 0 { for _, variant := range media.Variants { if variant.Quality == requested { variant.Variants = media.Variants media = variant break } } } lines := strings.Split(media.Playlist, "\n") variants := playbackHLSVariants(media.Playlist) if len(variants) == 0 { return media, nil } sort.SliceStable(variants, func(i, j int) bool { return variants[i].quality > variants[j].quality }) selected := variants[0] media.Variants = nil for _, variant := range variants { media.Variants = append(media.Variants, providerMedia{Quality: variant.quality}) if variant.quality == requested { selected = variant } } base, err := url.Parse(media.URL) if err != nil { return providerMedia{}, err } reference, err := url.Parse(selected.uri) if err != nil { return providerMedia{}, err } address := base.ResolveReference(reference).String() if !isProviderHTTPMediaURL(address) { return providerMedia{}, errors.New("清晰度播放列表地址无效") } playlist, err := downloader.fetchProviderText(ctx, address, media.Referer) if err != nil { return providerMedia{}, err } if !strings.HasPrefix(strings.TrimSpace(strings.TrimPrefix(playlist, "\ufeff")), "#EXTM3U") { return providerMedia{}, errors.New("站点未返回所选清晰度的播放列表") } filtered := make([]string, 0, len(lines)) for i := 0; i < len(lines); i++ { line := strings.TrimSpace(lines[i]) if strings.HasPrefix(line, "#EXT-X-STREAM-INF:") && i+1 < len(lines) { if i == selected.line { filtered = append(filtered, lines[i], lines[i+1]) } i++ continue } if !strings.HasPrefix(line, "#EXT-X-I-FRAME-STREAM-INF:") { filtered = append(filtered, lines[i]) } } media.Playlist, media.Quality = strings.Join(filtered, "\n"), selected.quality if duration := m3u8Duration(playlist); duration > 0 { media.Duration = duration } return media, nil } type playbackHLSVariant struct { line int quality int uri string } func playbackHLSVariants(playlist string) []playbackHLSVariant { lines := strings.Split(playlist, "\n") var variants []playbackHLSVariant for i := 0; i+1 < len(lines); i++ { line := strings.TrimSpace(lines[i]) if !strings.HasPrefix(line, "#EXT-X-STREAM-INF:") { continue } match := playbackHLSResolution.FindStringSubmatch(strings.TrimPrefix(line, "#EXT-X-STREAM-INF:")) if len(match) != 3 { continue } width, _ := strconv.Atoi(match[1]) height, _ := strconv.Atoi(match[2]) if width < height { height = width } uri := strings.TrimSpace(lines[i+1]) if height > 0 && height <= 4320 && uri != "" && !strings.HasPrefix(uri, "#") { variants = append(variants, playbackHLSVariant{line: i, quality: height, uri: uri}) } } return variants }