CRITICAL FIX: require title match, not just year, before adding to Sonarr
Build and Deploy / build-and-deploy (push) Successful in 2m14s
Build and Deploy / build-and-deploy (push) Successful in 2m14s
The title+year fallback added last commit had no title check at all - it accepted the first search candidate whose year merely matched (or ANY candidate at all when our year was null), silently adding a completely unrelated show to Sonarr. Confirmed in production: "Первый раз" (2025) resolved to "Around the World in 80 Weighs" (2025) - same year, nothing else in common. A candidate now must match both: title (against Sonarr's primary title OR any alternateTitles, since a Russian-titled search rarely hits TheTVDB's English primary title directly) AND year within 1, with neither side allowed to be null/absent as a wildcard. No confident match resolves to null (the existing 422 path) rather than ever guessing - many Russian-titled shows will now correctly fail to resolve instead of silently adding the wrong series. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
513eac8f80
commit
60dd95b93b
@@ -153,9 +153,19 @@ public class MediaActionController {
|
||||
* id-based lookup (SonarrClient.lookupByTmdbId), but this Sonarr
|
||||
* instance's /series/lookup doesn't recognize the "tmdb:" term prefix at
|
||||
* all (empirically zero results even for a well-known show) - so the
|
||||
* real fallback is a plain keyword search, picking the candidate whose
|
||||
* year matches ours (title alone is too unreliable: Sonarr's TheTVDB-
|
||||
* backed provider may not carry the same localized title we do).
|
||||
* real fallback is a plain keyword search.
|
||||
*
|
||||
* BUG HISTORY: an earlier version of this fallback accepted the first
|
||||
* candidate whose YEAR merely matched (or accepted anything at all when
|
||||
* our year was null), with no title check whatsoever - Sonarr silently
|
||||
* added a completely unrelated show ("Первый раз" (2025) resolved to
|
||||
* "Around the World in 80 Weighs" (2025), same year, nothing else in
|
||||
* common). A candidate must now match BOTH title and year - title
|
||||
* against Sonarr's primary title OR any of its alternateTitles (a
|
||||
* Russian-titled search rarely matches TheTVDB's English primary title
|
||||
* directly), and year within 1. No confident match -> null, never a
|
||||
* guess: an honest "couldn't find it" beats silently adding the wrong
|
||||
* series to someone's library.
|
||||
*/
|
||||
private Integer resolveTvdbId(MediaEntity media) {
|
||||
if (media.getTmdbId() != null) {
|
||||
@@ -166,15 +176,41 @@ public class MediaActionController {
|
||||
}
|
||||
for (JsonNode candidate : sonarrClient.searchByTitle(media.getTitle())) {
|
||||
if (!candidate.has("tvdbId") || candidate.get("tvdbId").asInt() <= 0) continue;
|
||||
Integer candidateYear = candidate.has("year") && !candidate.get("year").isNull()
|
||||
? candidate.get("year").asInt() : null;
|
||||
if (media.getYear() == null || candidateYear == null || Math.abs(candidateYear - media.getYear()) <= 1) {
|
||||
return candidate.get("tvdbId").asInt();
|
||||
}
|
||||
if (!candidateMatches(candidate, media)) continue;
|
||||
return candidate.get("tvdbId").asInt();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private boolean candidateMatches(JsonNode candidate, MediaEntity media) {
|
||||
Integer candidateYear = candidate.has("year") && !candidate.get("year").isNull()
|
||||
? candidate.get("year").asInt() : null;
|
||||
if (!yearMatches(candidateYear, media.getYear())) return false;
|
||||
|
||||
if (titleMatches(candidate.path("title").asText(null), media.getTitle())) return true;
|
||||
if (candidate.has("alternateTitles")) {
|
||||
for (JsonNode alt : candidate.get("alternateTitles")) {
|
||||
if (titleMatches(alt.path("title").asText(null), media.getTitle())) return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/** Exact match, or one title containing the other (handles subtitle/prefix variants). Both sides required. */
|
||||
private boolean titleMatches(String a, String b) {
|
||||
if (a == null || b == null) return false;
|
||||
String na = a.trim().toLowerCase();
|
||||
String nb = b.trim().toLowerCase();
|
||||
if (na.isEmpty() || nb.isEmpty()) return false;
|
||||
return na.equals(nb) || na.contains(nb) || nb.contains(na);
|
||||
}
|
||||
|
||||
/** A confident year match requires both sides present and within 1 - unlike the sourcing heuristics elsewhere, a missing year here is NOT treated as a wildcard. */
|
||||
private boolean yearMatches(Integer a, Integer b) {
|
||||
if (a == null || b == null) return false;
|
||||
return Math.abs(a - b) <= 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* On-demand "find recommendations for this release" from the detail
|
||||
* modal - runs once per media (see MediaEntity.onDemandRecommendationsFetchedAt),
|
||||
|
||||
Reference in New Issue
Block a user