136 lines
3.5 KiB
Go
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
|
|
}
|