Files
mal/api/playback/service_ranking.go

189 lines
4.3 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 := lookupPriority(sourceTypePriority, source.Type, 200)
providerScore := lookupPriority(providerPriority, source.Provider, 60)
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,
})
}
// stable sort to preserve insertion order for equal scores
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
}
var sourceTypePriority = map[string]int{
"mp4": 500,
"m3u8": 450,
"unknown": 300,
"embed": 100,
}
var providerPriority = map[string]int{
"s-mp4": 120,
"default": 115,
"luf-mp4": 110,
"vid-mp4": 105,
"yt-mp4": 100,
"mp4": 95,
"uv-mp4": 90,
"hls": 80,
"sw": 40,
"ok": 35,
"ss-hls": 30,
}
func lookupPriority(m map[string]int, key string, fallback int) int {
if p, ok := m[strings.ToLower(key)]; ok {
return p
}
return fallback
}
// sourceQualityPriority scores quality match: exact match gets boost, mismatch gets penalty.
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
}
}
// qualityMatches checks if source matches target by substring or extracted digits.
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
}
return extractDigits(sourceLower) == extractDigits(targetLower)
}
// parseQualityValue extracts numeric value from quality string.
func parseQualityValue(rawQuality string) int {
lower := strings.ToLower(rawQuality)
if lower == "auto" {
return 240
}
digits := extractDigits(lower)
if digits == "" {
return 0
}
value, err := strconv.Atoi(digits)
if err != nil {
return 0
}
return value
}
// extractDigits reads leading digits until a non-digit or break condition.
func extractDigits(value string) string {
var digits []byte
for _, char := range value {
if char >= '0' && char <= '9' {
digits = append(digits, byte(char))
} else if len(digits) > 0 {
break
}
}
return string(digits)
}
// normalizeSourceTypeFromProbe overrides source type based on Content-Type header.
func normalizeSourceTypeFromProbe(source StreamSource, contentType string) StreamSource {
lower := strings.ToLower(contentType)
switch {
case strings.Contains(lower, "video/mp4"):
source.Type = "mp4"
case strings.Contains(lower, "mpegurl"):
source.Type = "m3u8"
}
return source
}
// isLikelyMP4 checks ftyp box header (bytes 4-8 of mp4 files).
func isLikelyMP4(payload []byte) bool {
if len(payload) < 12 {
return false
}
return bytes.Equal(payload[4:8], []byte("ftyp"))
}
// isLikelyM3U8 checks for m3u8 file header.
func isLikelyM3U8(payload []byte) bool {
trimmed := strings.TrimSpace(string(payload))
return strings.HasPrefix(trimmed, "#EXTM3U")
}