fix(explore): match sequel catalog entries to their parent library show
MAL/AniList season entries never matched the library show they belong to. Both backends use the catalog title as a server-side filter before verifying external ids, and a title like "Mushoku Tensei: Jobless Reincarnation Season 2" cannot reach a show stored as "Mushoku Tensei: Jobless Reincarnation". Measured against a 267-show Plex library, 3 of 113 mapped sequel entries matched. The reverse lookup now takes two ordered title candidates instead of one: the entry's own title and its season-stripped form, with typographic punctuation normalised because both servers miss on a curly apostrophe. That matches 77 of 113. Widening it further to romaji/native/synonym variants reached only 81, so the cap stays at two rather than spending up to five more requests per lookup that finds nothing. A sequel's year is its own season's, not the parent show's, so the +/-1 year window is dropped for one - it would exclude the very show being looked for. That also keeps a miss at the two requests the single-title lookup already spent. A Plex Discover item additionally skips the title search entirely by filtering on the plex:// guid its own rating key already is, which costs no extra request and needs no cloud lookup. A season 2+ entry only matches when the server really has that season, which costs one children fetch on a match. Only a season both TVDB and TMDB agree on is gated: which provider a library numbers its seasons by is a server setting no dataset supplies and none of it is inferable from the ids an item exposes, so a disagreeing reference is left ungated rather than gated on a guess. The match cache is keyed per source and per entry rather than by canonical id, which every season of a series shares: all five Mushoku Tensei entries collapse to imdb:tt13293588, so one season-gated result would have poisoned the rest. Entries whose Fribb row carries no provider id at all remain unmatched. That is an upstream mapping gap, not something to guess around with extra lookups. close #1704
This commit is contained in:
@@ -11,6 +11,7 @@ class AnilistMedia {
|
||||
final String? titleEnglish;
|
||||
final String? titleRomaji;
|
||||
final String? titleUserPreferred;
|
||||
final String? titleNative;
|
||||
final String? format;
|
||||
final String? status;
|
||||
final int? episodes;
|
||||
@@ -21,6 +22,7 @@ class AnilistMedia {
|
||||
final int? seasonYear;
|
||||
final int? startYear;
|
||||
final List<String>? genres;
|
||||
final List<String>? synonyms;
|
||||
final bool isAdult;
|
||||
final String? coverImageExtraLarge;
|
||||
final String? coverImageLarge;
|
||||
@@ -35,6 +37,7 @@ class AnilistMedia {
|
||||
this.titleEnglish,
|
||||
this.titleRomaji,
|
||||
this.titleUserPreferred,
|
||||
this.titleNative,
|
||||
this.format,
|
||||
this.status,
|
||||
this.episodes,
|
||||
@@ -45,6 +48,7 @@ class AnilistMedia {
|
||||
this.seasonYear,
|
||||
this.startYear,
|
||||
this.genres,
|
||||
this.synonyms,
|
||||
this.isAdult = false,
|
||||
this.coverImageExtraLarge,
|
||||
this.coverImageLarge,
|
||||
@@ -67,6 +71,7 @@ class AnilistMedia {
|
||||
titleEnglish: title is Map ? title['english'] as String? : null,
|
||||
titleRomaji: title is Map ? title['romaji'] as String? : null,
|
||||
titleUserPreferred: title is Map ? title['userPreferred'] as String? : null,
|
||||
titleNative: title is Map ? title['native'] as String? : null,
|
||||
format: json['format'] as String?,
|
||||
status: json['status'] as String?,
|
||||
episodes: flexibleInt(json['episodes']),
|
||||
@@ -77,6 +82,7 @@ class AnilistMedia {
|
||||
seasonYear: flexibleInt(json['seasonYear']),
|
||||
startYear: startDate is Map ? flexibleInt(startDate['year']) : null,
|
||||
genres: _stringList(json['genres']),
|
||||
synonyms: _stringList(json['synonyms']),
|
||||
isAdult: json['isAdult'] == true,
|
||||
coverImageExtraLarge: coverImage is Map ? coverImage['extraLarge'] as String? : null,
|
||||
coverImageLarge: coverImage is Map ? coverImage['large'] as String? : null,
|
||||
|
||||
@@ -65,6 +65,24 @@ class CatalogItemIds {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Identity of *this* entry, preferring provider-native entry ids over the
|
||||
/// series ids it shares with its own other seasons.
|
||||
///
|
||||
/// [canonicalKey] deliberately prefers imdb/tmdb/tvdb so two sources
|
||||
/// describing the same title agree; that is exactly wrong for anything
|
||||
/// season-specific, because every MAL/AniList season of one series carries
|
||||
/// the same series ids. All five Mushoku Tensei entries collapse to
|
||||
/// `imdb:tt13293588` under [canonicalKey].
|
||||
String? get entryKey {
|
||||
if (mal != null) return 'mal:$mal';
|
||||
if (anilist != null) return 'anilist:$anilist';
|
||||
if (simkl != null) return 'simkl:$simkl';
|
||||
if (trakt != null) return 'trakt:$trakt';
|
||||
if (plex != null) return 'plex:$plex';
|
||||
if (slug != null) return 'slug:$slug';
|
||||
return canonicalKey;
|
||||
}
|
||||
|
||||
/// Every id-form key. Membership checks match on any of these so that two
|
||||
/// sides carrying different id subsets (e.g. Jellyfin tmdb-only vs a Trakt
|
||||
/// entry keyed by imdb) still intersect.
|
||||
@@ -120,6 +138,17 @@ class CatalogItem {
|
||||
/// [MediaKind.movie] or [MediaKind.show].
|
||||
final MediaKind kind;
|
||||
final String title;
|
||||
|
||||
/// Other titles the same entry is known by (MAL `alternative_titles`,
|
||||
/// AniList `romaji`/`native`/`synonyms`). Media servers index one localized
|
||||
/// title each, so these widen the reverse lookup without weakening it —
|
||||
/// candidates are still verified by exact external id.
|
||||
final List<String> altTitles;
|
||||
|
||||
/// Season of the parent series this entry maps to, when the provider covers
|
||||
/// one season of a longer show (Fribb `season`). Null for whole-series
|
||||
/// entries and every non-anime source.
|
||||
final ExternalSeasonRef? season;
|
||||
final int? year;
|
||||
final String? overview;
|
||||
final int? runtimeMinutes;
|
||||
@@ -149,6 +178,8 @@ class CatalogItem {
|
||||
required this.source,
|
||||
required this.kind,
|
||||
required this.title,
|
||||
this.altTitles = const [],
|
||||
this.season,
|
||||
this.year,
|
||||
this.overview,
|
||||
this.runtimeMinutes,
|
||||
@@ -168,6 +199,10 @@ class CatalogItem {
|
||||
/// Kind-namespaced identity key for caches and dedupe.
|
||||
String get identityKey => '${kind.id}/${ids.canonicalKey}';
|
||||
|
||||
/// Cache key for anything whose answer is season-specific — see
|
||||
/// [CatalogItemIds.entryKey], which [identityKey] deliberately does not use.
|
||||
String get entryIdentityKey => '${kind.id}/${ids.entryKey}';
|
||||
|
||||
/// Synthesize a [MediaItem] so catalog items flow through the existing
|
||||
/// shelf/grid/card stack ([MediaHub.items] is `List<MediaItem>`).
|
||||
///
|
||||
@@ -196,6 +231,8 @@ class CatalogItem {
|
||||
'source': source.name,
|
||||
'kind': kind.id,
|
||||
'title': title,
|
||||
if (altTitles.isNotEmpty) 'altTitles': altTitles,
|
||||
if (season != null) 'season': season!.toJson(),
|
||||
if (year != null) 'year': year,
|
||||
if (overview != null) 'overview': overview,
|
||||
if (runtimeMinutes != null) 'runtimeMinutes': runtimeMinutes,
|
||||
@@ -221,6 +258,12 @@ class CatalogItem {
|
||||
(throw ArgumentError('Unknown catalog source: ${json['source']}')),
|
||||
kind: MediaKind.fromString(json['kind'] as String?),
|
||||
title: json['title'] as String? ?? '',
|
||||
altTitles: (json['altTitles'] as List?)?.cast<String>() ?? const [],
|
||||
season: switch (json['season']) {
|
||||
final Map<String, Object?> s => ExternalSeasonRef.fromJson(s),
|
||||
final Map s => ExternalSeasonRef.fromJson(s.cast<String, Object?>()),
|
||||
_ => null,
|
||||
},
|
||||
year: json['year'] as int?,
|
||||
overview: json['overview'] as String?,
|
||||
runtimeMinutes: json['runtimeMinutes'] as int?,
|
||||
|
||||
Reference in New Issue
Block a user