fix: reject low-confidence automatic metadata matches

This commit is contained in:
ShukeBta
2026-06-25 02:58:07 +08:00
parent 9021007027
commit 5eb0d4c8af
5 changed files with 138 additions and 8 deletions
+6 -5
View File
@@ -93,11 +93,12 @@ func (b *BangumiProvider) Search(ctx context.Context, query string) (*Match, err
title = r.Name
}
m := &Match{
BangumiID: r.ID,
Title: title,
Overview: r.Summary,
PosterURL: normalizeBangumiImageURL(r.Images.Large),
Rating: r.Rating.Score,
BangumiID: r.ID,
Title: title,
OriginalName: r.Name,
Overview: r.Summary,
PosterURL: normalizeBangumiImageURL(r.Images.Large),
Rating: r.Rating.Score,
}
if len(r.Air) >= 4 {
_, _ = fmt.Sscanf(r.Air[:4], "%d", &m.Year)
+98
View File
@@ -0,0 +1,98 @@
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
}
}
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, 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 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
}
@@ -150,7 +150,7 @@ func organizeMetadataMatchTrusted(query string, sourceYear int, match *Match) bo
return false
}
}
return true
return automaticMetadataTitleTrusted(query, match)
}
func organizeMatchFromLocalMetadata(local *LocalMetadata) *Match {
+2 -2
View File
@@ -20,10 +20,10 @@ var noiseTokens = []string{
"hdrip", "bluray", "blu-ray", "webrip", "web-dl", "web",
"x264", "x265", "h264", "h265", "hevc", "avc", "10bit", "8bit", "hi10p", "hi10",
"hdr", "hdr10", "sdr", "dts", "ddp", "ddp5", "dd5", "dd2", "eac3", "truehd",
"dovi", "atmos", "aac", "ac3", "flac",
"dovi", "atmos", "aac", "ac3", "flac", "fps", "hlg", "dv",
"remux", "extended", "uncut", "remastered", "repack", "proper", "internal",
"limited", "imax", "directors-cut", "directors_cut",
"hkfree", "yify", "rarbg", "ettv", "fgt", "tgx", "ctrlhd", "ntb", "flux",
"hkfree", "yify", "rarbg", "ettv", "fgt", "tgx", "ctrlhd", "ntb", "flux", "qhstudio",
// 流媒体平台 / 字幕组 / 国家版本(动漫常见)
"netflix", "nf", "amzn", "hulu", "disney", "max", "hbo",
+31
View File
@@ -31,6 +31,7 @@ func TestCleanQuery(t *testing.T) {
{"亏成首富从游戏开始 The Richest in Game - S01E11 - 4K.mp4", "亏成首富从游戏开始 the richest in game", 0},
{"紫川.2024.S02E24.第24集.2160p.WEB-DL.H.265-ColorTV.mkv", "紫川", 2024},
{"紫川 (2024) {tmdb-247590}", "紫川", 2024},
{"HNTV.Spring.Festival.Gala.FPS.HLG-QHStudio.S01E202-DD5.QHstudIo.6.4K.ts", "hntv spring festival gala", 0},
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
@@ -301,6 +302,36 @@ func TestOrganizeMetadataRejectsEpisodeOnlyQuery(t *testing.T) {
}
}
func TestOrganizeMetadataTrustsExactOriginalTitle(t *testing.T) {
match := &Match{Title: "镖人", OriginalName: "Blades of the Guardians", Year: 2023, TMDbID: 107463}
if !organizeMetadataMatchTrusted("blades of the guardians", 2023, match) {
t.Fatal("exact original title should be trusted")
}
}
func TestOrganizeMetadataTrustsCleanedBroadcastReleaseQuery(t *testing.T) {
match := &Match{Title: "湖南卫视春节联欢晚会", OriginalName: "HNTV Spring Festival Gala", Year: 2026, TMDbID: 123456}
query := "hntv spring festival gala fps hlg qhstudio s01e qhstudio"
if !organizeMetadataMatchTrusted(query, 2026, match) {
t.Fatal("release noise should not block an otherwise matching title")
}
}
func TestOrganizeMetadataRejectsLooseSingleTokenQuery(t *testing.T) {
match := &Match{Title: "步步惊心泰版", OriginalName: "Scarlet Heart Thailand", Year: 2025, TMDbID: 252886}
if organizeMetadataMatchTrusted("thailand", 0, match) {
t.Fatal("single generic token must not trust a broader title")
}
}
func TestOrganizeMetadataRejectsDirtyUnrelatedQuery(t *testing.T) {
match := &Match{Title: "春节联欢晚会", OriginalName: "Spring Festival Gala", Year: 2026, TMDbID: 284725}
query := "hntv spring festival gala fps hlg qhstudio s01e qhstudio"
if organizeMetadataMatchTrusted(query, 2026, match) {
t.Fatal("dirty query with a missing broadcaster token must not trust a partial title")
}
}
func TestSeriesTitleFromMediaPathIgnoresEpisodeOnlyFolder(t *testing.T) {
got := seriesTitleFromMediaPath(`F:\media\电视剧\欧美剧\第 11 集 (2026)\Season 6\第 11 集 - S06E11.mkv`)
if got != "" {