fix: guard series metadata from episode identity pollution

This commit is contained in:
ShukeBta
2026-06-25 07:22:00 +08:00
parent 3419077b67
commit 087a00b5d0
8 changed files with 338 additions and 13 deletions
+56 -4
View File
@@ -212,26 +212,34 @@ func (s *ScraperService) matchFromMediaExternalIDs(ctx context.Context, m *model
mediaType = "tv"
}
if match := s.manualTMDbMatchByID(ctx, m.TMDbID, normalizeMediaType(mediaType, m.Title, "")); match != nil {
return match
if s.mediaExternalIDMatchTrusted(m, lib, match, "tmdb") {
return match
}
}
}
if strings.TrimSpace(m.DoubanID) != "" && s.douban != nil && s.douban.Enabled() {
if match, err := s.douban.GetMatchByID(ctx, strings.TrimSpace(m.DoubanID)); err == nil && match != nil {
return match
if s.mediaExternalIDMatchTrusted(m, lib, match, "douban") {
return match
}
} else if err != nil {
s.log.Debug("douban id lookup failed", zap.String("media_id", m.ID), zap.String("douban_id", m.DoubanID), zap.Error(err))
}
}
if m.BangumiID > 0 && s.bangumi != nil && s.bangumi.Enabled() {
if match, err := s.bangumi.GetSubject(ctx, m.BangumiID); err == nil && match != nil {
return match
if s.mediaExternalIDMatchTrusted(m, lib, match, "bangumi") {
return match
}
} else if err != nil {
s.log.Debug("bangumi id lookup failed", zap.String("media_id", m.ID), zap.Int("bangumi_id", m.BangumiID), zap.Error(err))
}
}
if strings.TrimSpace(m.TheTVDBID) != "" && s.thetvdb != nil && s.thetvdb.Enabled() {
if match, err := s.thetvdb.GetSeriesMatchByID(ctx, strings.TrimSpace(m.TheTVDBID)); err == nil && match != nil {
return match
if s.mediaExternalIDMatchTrusted(m, lib, match, "thetvdb") {
return match
}
} else if err != nil {
s.log.Debug("thetvdb id lookup failed", zap.String("media_id", m.ID), zap.String("thetvdb_id", m.TheTVDBID), zap.Error(err))
}
@@ -239,6 +247,35 @@ func (s *ScraperService) matchFromMediaExternalIDs(ctx context.Context, m *model
return nil
}
func (s *ScraperService) mediaExternalIDMatchTrusted(m *model.Media, lib *model.Library, match *Match, source string) bool {
if match == nil || strings.TrimSpace(match.Title) == "" {
return false
}
if !mediaIsEpisodic(m, lib) {
return true
}
for _, candidate := range scrapeQueryCandidates(m, lib) {
if unsafeAutomaticEpisodeQuery(candidate) {
continue
}
if automaticMetadataTitleTrusted(candidate, match) {
return true
}
}
if s != nil && s.log != nil && m != nil {
s.log.Warn("episode external id match rejected",
zap.String("media_id", m.ID),
zap.String("source", source),
zap.String("title", match.Title),
zap.String("original_name", match.OriginalName),
zap.Int("tmdb_id", match.TMDbID),
zap.Int("bangumi_id", match.BangumiID),
zap.String("douban_id", match.DoubanID),
zap.String("thetvdb_id", match.TheTVDBID))
}
return false
}
func (s *ScraperService) applyFanartArtwork(ctx context.Context, match *Match) {
if s == nil || s.fanart == nil || !s.fanart.Enabled() || match == nil {
return
@@ -274,6 +311,12 @@ func mediaYearHint(m *model.Media) int {
if m == nil {
return 0
}
if m.SeasonNum > 0 || m.EpisodeNum > 0 {
return seriesPathYearHint(m.Path)
}
if season, episode := ParseEpisode(m.Path); season > 0 || episode > 0 {
return seriesPathYearHint(m.Path)
}
if m.Year > 0 {
return m.Year
}
@@ -283,6 +326,15 @@ func mediaYearHint(m *model.Media) int {
return yearFromText(m.Path)
}
func seriesPathYearHint(path string) int {
if showDir := showDirFromEpisodePath(path); showDir != "" {
if _, year := CleanQuery(showDir); year > 0 {
return year
}
}
return 0
}
func yearFromText(raw string) int {
if raw == "" {
return 0