217 lines
4.1 KiB
Go
217 lines
4.1 KiB
Go
package playback
|
|
|
|
import (
|
|
"bytes"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
func rankSources(sources []StreamSource, quality string) ([]sourceScore, error) {
|
|
filtered := make([]StreamSource, 0, len(sources))
|
|
seen := make(map[string]struct{})
|
|
|
|
for _, source := range sources {
|
|
if source.URL == "" {
|
|
continue
|
|
}
|
|
if _, exists := seen[source.URL]; exists {
|
|
continue
|
|
}
|
|
seen[source.URL] = struct{}{}
|
|
filtered = append(filtered, source)
|
|
}
|
|
|
|
if len(filtered) == 0 {
|
|
return nil, errors.New("no playable sources available")
|
|
}
|
|
|
|
targetQuality := normalizeQuality(quality)
|
|
scored := make([]sourceScore, 0, len(filtered))
|
|
for _, source := range filtered {
|
|
typeScore := sourceTypePriority(source.Type)
|
|
providerScore := providerPriority(source.Provider)
|
|
qualityScore := sourceQualityPriority(source.Quality, targetQuality)
|
|
refererScore := 0
|
|
if source.Referer != "" {
|
|
refererScore = 20
|
|
}
|
|
|
|
total := typeScore + providerScore + qualityScore + refererScore
|
|
scored = append(scored, sourceScore{
|
|
source: source,
|
|
total: total,
|
|
typeScore: typeScore,
|
|
providerScore: providerScore,
|
|
qualityScore: qualityScore,
|
|
refererScore: refererScore,
|
|
})
|
|
}
|
|
|
|
sort.SliceStable(scored, func(i int, j int) bool {
|
|
return scored[i].total > scored[j].total
|
|
})
|
|
|
|
return scored, nil
|
|
}
|
|
|
|
func normalizeQuality(quality string) string {
|
|
lower := strings.ToLower(strings.TrimSpace(quality))
|
|
if lower == "" {
|
|
return "best"
|
|
}
|
|
|
|
return lower
|
|
}
|
|
|
|
func sourceTypePriority(sourceType string) int {
|
|
switch strings.ToLower(sourceType) {
|
|
case "mp4":
|
|
return 500
|
|
case "m3u8":
|
|
return 450
|
|
case "unknown":
|
|
return 300
|
|
case "embed":
|
|
return 100
|
|
default:
|
|
return 200
|
|
}
|
|
}
|
|
|
|
func providerPriority(provider string) int {
|
|
switch strings.ToLower(provider) {
|
|
case "s-mp4":
|
|
return 120
|
|
case "default":
|
|
return 115
|
|
case "luf-mp4":
|
|
return 110
|
|
case "vid-mp4":
|
|
return 105
|
|
case "yt-mp4":
|
|
return 100
|
|
case "mp4":
|
|
return 95
|
|
case "uv-mp4":
|
|
return 90
|
|
case "hls":
|
|
return 80
|
|
case "sw":
|
|
return 40
|
|
case "ok":
|
|
return 35
|
|
case "ss-hls":
|
|
return 30
|
|
default:
|
|
return 60
|
|
}
|
|
}
|
|
|
|
func sourceQualityPriority(sourceQuality string, targetQuality string) int {
|
|
qualityValue := parseQualityValue(sourceQuality)
|
|
|
|
switch targetQuality {
|
|
case "best":
|
|
return qualityValue
|
|
case "worst":
|
|
return -qualityValue
|
|
default:
|
|
if qualityMatches(sourceQuality, targetQuality) {
|
|
return 2000 + qualityValue
|
|
}
|
|
|
|
return -300 + qualityValue
|
|
}
|
|
}
|
|
|
|
func parseQualityValue(rawQuality string) int {
|
|
lower := strings.ToLower(rawQuality)
|
|
var digits strings.Builder
|
|
|
|
for _, char := range lower {
|
|
if char >= '0' && char <= '9' {
|
|
digits.WriteRune(char)
|
|
continue
|
|
}
|
|
if digits.Len() > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
if digits.Len() > 0 {
|
|
value, err := strconv.Atoi(digits.String())
|
|
if err == nil {
|
|
return value
|
|
}
|
|
}
|
|
|
|
if lower == "auto" {
|
|
return 240
|
|
}
|
|
|
|
return 0
|
|
}
|
|
|
|
func qualityMatches(sourceQuality string, targetQuality string) bool {
|
|
sourceLower := strings.ToLower(sourceQuality)
|
|
targetLower := strings.ToLower(targetQuality)
|
|
|
|
if sourceLower == "" {
|
|
return false
|
|
}
|
|
|
|
if strings.Contains(sourceLower, targetLower) {
|
|
return true
|
|
}
|
|
|
|
sourceDigits := extractDigits(sourceLower)
|
|
targetDigits := extractDigits(targetLower)
|
|
|
|
return sourceDigits != "" && sourceDigits == targetDigits
|
|
}
|
|
|
|
func extractDigits(value string) string {
|
|
var digits strings.Builder
|
|
for _, char := range value {
|
|
if char >= '0' && char <= '9' {
|
|
digits.WriteRune(char)
|
|
continue
|
|
}
|
|
if digits.Len() > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
return digits.String()
|
|
}
|
|
|
|
func normalizeSourceTypeFromProbe(source StreamSource, contentType string) StreamSource {
|
|
lower := strings.ToLower(contentType)
|
|
if strings.Contains(lower, "video/mp4") {
|
|
source.Type = "mp4"
|
|
return source
|
|
}
|
|
|
|
if strings.Contains(lower, "mpegurl") {
|
|
source.Type = "m3u8"
|
|
return source
|
|
}
|
|
|
|
return source
|
|
}
|
|
|
|
func isLikelyMP4(payload []byte) bool {
|
|
if len(payload) < 12 {
|
|
return false
|
|
}
|
|
|
|
return bytes.Equal(payload[4:8], []byte("ftyp"))
|
|
}
|
|
|
|
func isLikelyM3U8(payload []byte) bool {
|
|
trimmed := strings.TrimSpace(string(payload))
|
|
return strings.HasPrefix(trimmed, "#EXTM3U")
|
|
}
|