Files
silo-server/internal/subtitles/scoring.go
T

136 lines
3.5 KiB
Go

// internal/subtitles/scoring.go
package subtitles
import (
"fmt"
"strings"
)
// Scoring point values from spec.
const (
episodeTitlePoints = 120
episodeYearPoints = 60
episodeSeasonPoints = 20
episodeEpisodePoints = 20
movieTitlePoints = 60
movieYearPoints = 30
groupPoints = 15
sourcePoints = 8
audioCodecPoints = 3
resolutionPoints = 2
videoCodecPoints = 2
)
// ScoreResult scores a subtitle result against media metadata.
// isEpisode should be true for TV episodes (season > 0).
// Returns a score 0-100.
func ScoreResult(result SubtitleResult, req SearchRequest) float64 {
isEpisode := req.Season > 0
subInfo := ParseReleaseInfo(result.ReleaseName)
subName := normalizeTitle(result.ReleaseName)
var matched, maxPoints float64
if isEpisode {
maxPoints += episodeTitlePoints
if titleMatches(subName, req.Title) {
matched += episodeTitlePoints
}
if req.Year > 0 {
maxPoints += episodeYearPoints
if strings.Contains(subName, fmt.Sprintf("%d", req.Year)) {
matched += episodeYearPoints
}
}
if req.Season > 0 {
maxPoints += episodeSeasonPoints
if seasonMatches(subName, req.Season) {
matched += episodeSeasonPoints
}
}
if req.Episode > 0 {
maxPoints += episodeEpisodePoints
if episodeMatches(subName, req.Episode) {
matched += episodeEpisodePoints
}
}
} else {
maxPoints += movieTitlePoints
if titleMatches(subName, req.Title) {
matched += movieTitlePoints
}
if req.Year > 0 {
maxPoints += movieYearPoints
if strings.Contains(subName, fmt.Sprintf("%d", req.Year)) {
matched += movieYearPoints
}
}
}
// MediaInfo fields always count toward maxPoints when present in the request,
// so that missing metadata in the subtitle name is penalized.
maxPoints += groupPoints + sourcePoints + audioCodecPoints + resolutionPoints + videoCodecPoints
if req.MediaInfo != nil {
if req.MediaInfo.ReleaseGroup != "" && strings.EqualFold(subInfo.ReleaseGroup, req.MediaInfo.ReleaseGroup) {
matched += groupPoints
}
if req.MediaInfo.Source != "" && strings.EqualFold(subInfo.Source, req.MediaInfo.Source) {
matched += sourcePoints
}
if req.MediaInfo.AudioCodec != "" && strings.EqualFold(subInfo.AudioCodec, req.MediaInfo.AudioCodec) {
matched += audioCodecPoints
}
if req.MediaInfo.Resolution != "" && strings.EqualFold(subInfo.Resolution, req.MediaInfo.Resolution) {
matched += resolutionPoints
}
if req.MediaInfo.VideoCodec != "" && strings.EqualFold(subInfo.VideoCodec, req.MediaInfo.VideoCodec) {
matched += videoCodecPoints
}
}
if maxPoints == 0 {
return 0
}
return (matched / maxPoints) * 100
}
func normalizeTitle(s string) string {
return strings.ToLower(strings.NewReplacer(".", " ", "_", " ", "-", " ").Replace(s))
}
func titleMatches(subName, title string) bool {
if title == "" {
return false
}
return strings.Contains(subName, strings.ToLower(title))
}
func seasonMatches(subName string, season int) bool {
patterns := []string{
strings.ToLower(fmt.Sprintf("s%02d", season)),
strings.ToLower(fmt.Sprintf("s%d", season)),
strings.ToLower(fmt.Sprintf("season %d", season)),
}
for _, p := range patterns {
if strings.Contains(subName, p) {
return true
}
}
return false
}
func episodeMatches(subName string, episode int) bool {
patterns := []string{
strings.ToLower(fmt.Sprintf("e%02d", episode)),
strings.ToLower(fmt.Sprintf("e%d", episode)),
}
for _, p := range patterns {
if strings.Contains(subName, p) {
return true
}
}
return false
}