Files
MeBox/internal/service/metadata_match_trust.go
T
2026-06-26 08:42:16 +08:00

183 lines
4.5 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package service
import (
"regexp"
"strings"
)
var (
metadataTrustTokenRE = regexp.MustCompile(`[\p{L}\p{N}]+`)
metadataTrustDanglingEpisodeRE = regexp.MustCompile(`(?i)^s\d{1,2}e$`)
)
func automaticMetadataTitleTrusted(query string, match *Match) bool {
queryKey := metadataTrustKey(query)
if queryKey == "" || match == nil {
return false
}
for _, title := range []string{match.Title, match.OriginalName} {
titleKey := metadataTrustKey(title)
if titleKey == "" {
continue
}
if queryKey == titleKey || metadataTrustTokenOverlap(queryKey, titleKey) {
return true
}
}
if metadataTrustChineseReleaseAlias(queryKey, match) {
return true
}
if metadataTrustLocalizedSearchKeyword(queryKey, match) {
return true
}
return false
}
func metadataTrustKey(value string) string {
value = strings.ToLower(strings.TrimSpace(value))
if value == "" {
return ""
}
value = bracketedTag.ReplaceAllString(value, " ")
value = yearPattern.ReplaceAllString(value, " ")
for _, re := range []*regexp.Regexp{patSEnE, patDanglingSE, patNxE, patEP, patCN, patSeasonOnly, patCNSeason} {
value = re.ReplaceAllString(value, " ")
}
tokens := metadataTrustTokenRE.FindAllString(value, -1)
out := make([]string, 0, len(tokens))
for _, token := range tokens {
token = strings.TrimSpace(token)
if token == "" || metadataTrustNoiseToken(token) {
continue
}
out = append(out, token)
}
return strings.Join(out, " ")
}
func metadataTrustNoiseToken(token string) bool {
if token == "x" || token == "×" {
return true
}
if _, ok := noiseTokenSet[token]; ok {
return true
}
if metadataTrustDanglingEpisodeRE.MatchString(token) {
return true
}
return false
}
func metadataTrustTokenOverlap(queryKey, titleKey string) bool {
queryTokens := metadataTrustSignificantTokens(queryKey)
titleTokens := metadataTrustSignificantTokens(titleKey)
if len(queryTokens) <= 1 || len(titleTokens) <= 1 {
return false
}
titleSet := make(map[string]struct{}, len(titleTokens))
for _, token := range titleTokens {
titleSet[token] = struct{}{}
}
overlap := 0
for _, token := range queryTokens {
if _, ok := titleSet[token]; ok {
overlap++
}
}
queryCoverage := float64(overlap) / float64(len(queryTokens))
titleCoverage := float64(overlap) / float64(len(titleTokens))
return queryCoverage >= 0.80 && titleCoverage >= 0.50
}
func metadataTrustChineseReleaseAlias(queryKey string, match *Match) bool {
if match == nil || !metadataMatchHasChineseOrigin(match) {
return false
}
tokens := metadataTrustSignificantTokens(queryKey)
if len(tokens) < 2 {
return false
}
latinTokens := 0
for _, token := range tokens {
if containsLatin(token) {
latinTokens++
}
}
return latinTokens >= 2
}
func metadataTrustLocalizedSearchKeyword(queryKey string, match *Match) bool {
if match == nil || !metadataMatchHasExternalID(match) {
return false
}
searchKey := metadataTrustKey(match.SearchKeyword)
return searchKey != "" && searchKey == queryKey && metadataTrustStrongCJKQuery(queryKey)
}
func preferLocalizedSearchTitle(query string, match *Match) {
if match == nil || containsCJK(match.Title) {
return
}
if !metadataTrustLocalizedSearchKeyword(metadataTrustKey(query), match) {
return
}
localized := strings.TrimSpace(match.SearchKeyword)
if localized == "" {
localized = strings.TrimSpace(query)
}
if localized == "" {
return
}
if strings.TrimSpace(match.OriginalName) == "" {
match.OriginalName = strings.TrimSpace(match.Title)
}
match.Title = localized
}
func metadataTrustStrongCJKQuery(queryKey string) bool {
cjk := 0
for _, r := range queryKey {
switch {
case r >= '\u3400' && r <= '\u4dbf':
cjk++
case r >= '\u4e00' && r <= '\u9fff':
cjk++
case r >= '\uf900' && r <= '\ufaff':
cjk++
}
}
return cjk >= 4
}
func metadataMatchHasExternalID(match *Match) bool {
return match != nil &&
(match.TMDbID > 0 ||
match.BangumiID > 0 ||
strings.TrimSpace(match.DoubanID) != "" ||
strings.TrimSpace(match.TheTVDBID) != "")
}
func metadataMatchHasChineseOrigin(match *Match) bool {
if match == nil {
return false
}
languages := normalizeTokens(match.Languages...)
countries := normalizeTokens(match.Countries...)
return hasAny(languages, "ZH", "ZH-CN", "ZH-TW", "CN") ||
hasAny(countries, "CN", "TW", "HK", "MO")
}
func metadataTrustSignificantTokens(key string) []string {
fields := strings.Fields(key)
out := make([]string, 0, len(fields))
for _, field := range fields {
switch field {
case "the", "a", "an", "of", "and":
continue
default:
out = append(out, field)
}
}
return out
}