Skip to content

Fall back to an entry's other known IDs when no addon serves its canonical ID - #459

Open
Mitsu03 wants to merge 2 commits into
NuvioMedia:Devfrom
Mitsu03:fix/simkl-season-specific-imdb-fallback
Open

Mitsu03 wants to merge 2 commits into
NuvioMedia:Devfrom
Mitsu03:fix/simkl-season-specific-imdb-fallback

Conversation

@Mitsu03

@Mitsu03 Mitsu03 commented Aug 24, 2026

Copy link
Copy Markdown

Fixes #483.

Old behavior

SimklMedia.canonicalContentId() picks one ID and the meta lookup either resolves it or gives up.
For anime under Prefer IMDB that ID is whatever imdb value Simkl attached to that entry.

Broken behavior

Simkl gives some anime seasons their own IMDB title. Re:Zero season 4 is reported as tt36501927,
and Cinemeta answers GET /meta/series/tt36501927.json with 200 {}. The entry then cannot be
opened at all:

  1. resolveMetaLookupId returns the ID unchanged — it only rewrites tmdb: IDs.
  2. findMetaManifests offers only Cinemeta, since manifest idPrefixes filtering excludes every
    anime-ID addon for a tt ID.
  3. Cinemeta's {} is correctly treated as a miss — MetaDetailsParser throws and tryFetchMeta
    returns null — but there is nothing left to try.
  4. tryFetchTmdbFallbackMeta returns null immediately: fetchStandaloneMeta bails unless the ID
    starts with tmdb:.
  5. The screen ends at details_load_failed_all_addons.

Meanwhile the same Simkl entry carries mal:61316, anidb:19242, anilist:189046, kitsu:49746,
and four sibling Re:Zero entries on the same TVDB series carry the parent tt5607616 — which
Cinemeta serves with season 4 and correct air dates. None of it was consulted.

New behavior

SimklSyncSnapshot.alternateContentIdsFor(contentId) returns the other IDs the matching entry is
known under. MetaDetailsRepository consults it only after every addon and the TMDB fallback have
already missed
, so the normal path is unchanged and costs nothing.

Sibling IMDB IDs are tried first, most common first. Simkl models each anime season as its own entry
but keeps the franchise TVDB ID on all of them, so the seasons that do share a parent IMDB ID
identify that parent for the season that does not. Preferring them keeps Cinemeta as the metadata
source — and with it correct episode air dates — instead of silently moving one title to a different
metadata provider. That is also what the Prefer IMDB option already describes: "Group anime
seasons under a shared IMDB ID"
. The entry's own MAL/AniDB/AniList/Kitsu IDs follow as a last resort.

Reuses the existing alternateContentIds(), which was already computed for watched-key emission; it
only changes from private to internal.

How this was tested

Unit: three tests in SimklProjectionsTest covering the real Re:Zero ID set — that the franchise
IMDB ID is returned and ordered ahead of the anime IDs, that anime IDs are still used when no sibling
shares the TVDB series, and that an unknown content ID yields nothing. SimklProjectionsTest is
14/14.

Full suite: :composeApp:desktopTest — 790 tests, 1 failure in
NativePlayerControllerTeardownTest.sourceGapsDisposeSuccessfulSupersededCreatesBeforeReplacementPublication,
which reproduces identically on a clean checkout of Dev with these changes stashed. Unrelated to
this PR.

Against a real library: replayed the resolution logic over a 1283-entry Simkl library
(1099 unique (id, type) pairs) with Cinemeta and AIOStreams installed. 43 entries could not be
opened before; 38 resolve after — 12 via the franchise IMDB ID, 26 via an anime ID. Full per-title
breakdown in #483. The 5 remaining are movies carrying only a TMDB ID, which need a configured TMDB
key rather than a code change.

Manually verified on Windows with a patched local build: the Re:Zero season 4 Library entry opens on
the parent title, which is also where Continue Watching already points and where that show's watch
history is stored.

Scope

Three files, +187/-2, no dependency, UI, or architecture changes. The new lookup runs only on the
path that previously ended in an error.


Second commit: reseed Next Up when an entry resolves through a sibling ID

Found by running this branch on Windows for a day. The fallback above turns a dead meta lookup into
a live one, and Home's Next Up was not ready for a candidate whose metadata belongs to a different
ID than its watch history. Two finished shows started offering episodes already watched:

Card Seed Entry
Demon Slayer: Kimetsu no Yaiba — S4E1 "Someone's Dream" S3E11 tt15757634Entertainment District Arc
JoJo's Bizarre Adventure — S2E25 S2E24 tt3687376Stardust Crusaders

Both entries are Cinemeta misses — tt15757634 answers 200 {}, tt3687376 answers 200 with no
name field, which MetaDetailsParser requires — so both reach the new fallback and resolve through
the franchise IMDB ID their TVDB siblings carry. They come back with the whole franchise's episode
list, while latestCompletedSeriesEpisode still filters watch history by the candidate's own content
ID, where only that arc's episodes are recorded:

tt15757634 history: S3E1-11          → next after S3E11 = S4E1  (watched, under tt9335498)
tt9335498  history: S1, S2, S4, S5   → next after S5E8  = nothing

reanchorHomeNextUpCandidate() moves the candidate onto the ID the metadata actually came from and
recomputes the seed from that ID's history, so both halves are read on one identity. It also
collapses the arc candidate onto the franchise's own candidate, so one series cannot produce two
cards. A candidate that reseeds onto nothing, or onto a season-zero special, is dropped rather than
guessed at — the same rule buildHomeNextUpSeedCandidates already applies to its seeds. Nothing
changes when the addon serves the requested ID, which is every entry that resolved before this PR.

Tested by replaying the Next Up pipeline over the same 1284-entry library with the live Cinemeta
responses: the "before" run reproduces both cards exactly as they appeared on the Home screen, down
to the episode titles, and the "after" run produces neither. Five tests in HomeScreenTest cover the
reseeding, the untouched path, both drop conditions, and the dismiss-key collapse. Manually verified
on Windows 11 with a local build installed over 1.1.20 — both cards are gone from Continue Watching
and the rest of the rail is unchanged. Full reasoning and the replay output are in
this comment.

:composeApp:desktopTest for the branch as a whole is now 796 tests, 0 failures — the
NativePlayerControllerTeardownTest flake noted above did not reproduce.

Three files, +192/-6, two of which this PR already touches.

@Mitsu03 Mitsu03 changed the title Fall back to an entry other known IDs when no addon serves its canonical ID Fall back to an entry's other known IDs when no addon serves its canonical ID Aug 24, 2026
@Mitsu03

Mitsu03 commented Aug 24, 2026

Copy link
Copy Markdown
Author

Validated at runtime, on top of the 0.1.20-alpha release tag rather than Dev, so the only
difference from the shipped build is this changeset (b32dd57b + this commit, cherry-picked clean).

29 entries recovered in one session, 0 unresolved. Every Retrying meta for id=... had a
matching Resolved meta for id=...; 11 resolved through the franchise IMDB ID, keeping Cinemeta as
the metadata source, and 18 through an anime ID. The reported case:

Debug: (MetaDetailsRepo) Retrying meta for id=tt36501927 with alternate ids=[tt5607616,
       tmdb:328061, tvdb:305089, mal:61316, anidb:19242, anilist:189046, kitsu:49746, simkl:2743422]
Info:  (MetaDetailsRepo) Resolved meta for id=tt36501927 via alternate id=tt5607616

First candidate, as intended — the entry opens on the parent title with season 4 and its correct air
dates, instead of failing.

Other franchise-IMDB recoveries in the same session:

tt15757634 -> tt9335498    tt12866332 -> tt6185782    tt21599874 -> tt9522300
tt3249632  -> tt2560140    tt3687376  -> tt2359704    tt9696432  -> tt8254880
tt20753776 -> tt3114358    tt6213810  -> tt9525238    tt0805540  -> tt0481256

Two things the live run surfaced that the offline analysis had not:

The fallback also recovers non-IMDB canonical IDs. kitsu:41083 resolved to tt9458304, and
tmdb:295494, tmdb:71743, tmdb:87306 all recovered too. The chain is not anime-specific — any
entry whose canonical ID no addon serves gets the same treatment, and where the alternate happens to
be an IMDB ID the metadata improves rather than degrades.

Cinemeta has a second failure mode besides {}. tt3687376 came back well-formed enough to
parse but without a name, producing IllegalStateException: Missing required field 'name' instead
of Response did not contain a valid meta object (22 of the latter vs 1 of the former in this
session). Both are already caught by the existing try/catch in tryFetchMeta, so both reach the
new fallback — no extra handling needed, but worth recording since a narrower fix targeting only the
empty-object case would have missed it.

Full-suite state is unchanged from the PR description: :composeApp:desktopTest 790 tests, the one
NativePlayerControllerTeardownTest failure reproducing identically on a clean checkout without
these changes.

@Mitsu03

Mitsu03 commented Aug 25, 2026

Copy link
Copy Markdown
Author

Pushed a second commit — f61320b — after running this branch as a daily driver on Windows and finding that the fallback introduces a Continue Watching regression of its own. Better caught here than after merge.

What went wrong

Two entries appeared under Next Up on Home for shows that were finished months or years ago:

Card Seed the card was built from Entry it came from
Demon Slayer: Kimetsu no Yaiba — S4E1 "Someone's Dream" S3E11 tt15757634Entertainment District Arc
JoJo's Bizarre Adventure — S2E25 S2E24 tt3687376Stardust Crusaders

Both offered episodes already watched, under a different Simkl entry.

Why the fallback causes it

Both entries carry a season-specific IMDB ID and both are misses for Cinemeta, so both now reach tryFetchMetaByAlternateId:

  • GET /meta/series/tt15757634.json200 {} — the case this PR was written for.
  • GET /meta/series/tt3687376.json200 with a meta object that has no name field. MetaDetailsParser requires name, so it throws and tryFetchMeta returns null — a miss for the same reason, just a different shape of bad response.

Each then resolves through the franchise IMDB ID its TVDB siblings carry (tt9335498, tt2359704) and comes back with the whole franchise's episode list.

The seed does not follow. latestCompletedSeriesEpisode filters watch history by the candidate's own content ID, and the sibling arcs' watched episodes are recorded under the franchise ID, not this one. So the episode list is franchise-wide while the history is one arc's:

tt15757634 history: S3E1-11          → next after S3E11 = S4E1  (watched, under tt9335498)
tt9335498  history: S1, S2, S4, S5   → next after S5E8  = nothing

Before this PR neither entry produced a card at all: the meta lookup failed, resolveHomeNextUpCandidate returned transientFailure(), and that was that. The fallback turns a dead lookup into a live one, and Next Up was not ready for a candidate whose metadata belongs to a different ID than its history.

The fix

reanchorHomeNextUpCandidate() moves the candidate onto the ID the metadata actually came from and recomputes the seed from that ID's history. The episode list and the history are then read on one identity. It also collapses the arc candidate onto the franchise's own candidate, so a single series cannot produce two Next Up cards.

A candidate that reseeds onto nothing, or onto a season-zero special, is dropped rather than guessed at — the same rule buildHomeNextUpSeedCandidates already applies to its own seeds.

Nothing changes when the addon serves the requested ID, which is every entry that resolved before the fallback existed.

Testing

Replay over the real library. Re-ran the Next Up pipeline over a 1284-entry Simkl snapshot with the live Cinemeta responses, before and after:

Demon Slayer  arc tt15757634 -> meta resolved via tt9335498
   seed BEFORE (arc history)      : S3E11
   seed AFTER  (franchise history): S5E8
   BEFORE -> card S4E1 "Someone's Dream"   (already watched under tt9335498)
   AFTER  -> no episode after S5E8: NO CARD

JoJo  arc tt3687376 -> meta resolved via tt2359704
   seed BEFORE (arc history)      : S2E24
   seed AFTER  (franchise history): S3E39
   BEFORE -> card S2E25                    (already watched under tt2359704)
   AFTER  -> collapses onto the franchise seed tt2359704|3|39

The "before" run reproduces the two cards exactly as they appeared on the Home screen, down to the episode titles, which is what makes the "after" run meaningful.

Unit: five tests in HomeScreenTest — the reseeding, the untouched path when the addon served the requested ID, both drop conditions, and the dismiss-key collapse.

Full suite: :composeApp:desktopTest — 796 tests, 0 failures. (The NativePlayerControllerTeardownTest flake noted in the original description did not reproduce this run.)

Manual: Windows 11, local build of this branch installed over 1.1.20. Both cards are gone from Continue Watching; the rest of the rail is unchanged.

Scope

Three files, +192/-6. Two of them are the same files this PR already touches. No dependency, UI, or architecture changes.

A Simkl anime season can carry a season-specific IMDB ID that no installed
meta addon indexes. Re:Zero season 4 is reported by Simkl as tt36501927,
which Cinemeta answers with an empty object, so the Library entry cannot be
opened at all: findMetaManifests only offers Cinemeta for a tt ID, and
tryFetchTmdbFallbackMeta bails unless the ID starts with tmdb:.

The IDs needed to recover are already known. Add alternateContentIdsFor(),
which returns the other IDs the matching Simkl entry is known under, and
consult it from MetaDetailsRepository only after every addon and the TMDB
fallback have already missed. The normal path is untouched.

Sibling IMDB IDs come first. Simkl models each anime season as its own entry
but keeps the franchise TVDB ID on all of them, so the seasons sharing a
parent IMDB ID identify that parent for the season that does not - four
Re:Zero entries carry tt5607616 against one carrying tt36501927. Preferring
those keeps Cinemeta as the metadata source, and with it correct episode air
dates, which is also what the "Prefer IMDB" option already promises. The
entry's own MAL/AniDB/AniList/Kitsu IDs follow as a last resort.

Measured against a 1283-entry Simkl library: 43 entries could not be opened,
38 now resolve - 12 via the franchise IMDB ID, 26 via an anime ID. The
remaining 5 are movies carrying only a TMDB ID, which need a configured TMDB
key rather than a code change.

Fixes NuvioMedia#457
The alternate-ID fallback added in the previous commit lets an entry whose own
ID no addon serves borrow a sibling's metadata. Home's Next Up then reads a
franchise episode list against a single arc's watch history, and offers an
episode the viewer already watched under a sibling entry.

Two Simkl anime entries carrying a season-specific IMDB ID reproduce it:

  Demon Slayer: Entertainment District Arc  tt15757634  TVDB S3E1-11
  JoJo's Bizarre Adventure: Stardust Crusaders  tt3687376  TVDB S2E1-24

Cinemeta answers tt15757634 with 200 {} and tt3687376 with a meta object that
has no name field, which MetaDetailsParser requires. Both are misses, so both
now fall back to the franchise IMDB ID their TVDB siblings carry - tt9335498
and tt2359704 - and come back with the whole franchise's episodes. The seed
stays the arc's own finale, because the sibling arcs' watched episodes are
recorded under the franchise ID, not this one. Next after S3E11 is S4E1
"Someone's Dream"; next after S2E24 is S2E25. Both already watched.

reanchorHomeNextUpCandidate() moves the candidate onto the ID the metadata
actually came from and recomputes the seed from that ID's history, so the
episode list and the history are read on one identity. It also collapses the
arc candidate onto the franchise's own candidate, so one series cannot produce
two cards. A candidate that reseeds onto nothing, or onto a season-zero
special, is dropped rather than guessed at - the same rule
buildHomeNextUpSeedCandidates already applies to its seeds.

Nothing changes when the addon serves the requested ID, which is every entry
that resolved before the fallback existed.

Replayed over a 1284-entry Simkl library with the real Cinemeta responses:
before, the two entries above produce exactly the cards seen on the Home
screen; after, Demon Slayer has no episode past its S5E8 seed and JoJo
collapses onto the franchise seed. Verified on Windows with a local build -
both cards are gone from Continue Watching.

Five tests in HomeScreenTest cover the reseeding, the untouched path, both
drop conditions, and the dismiss-key collapse. :composeApp:desktopTest is
796/796.
@Mitsu03
Mitsu03 force-pushed the fix/simkl-season-specific-imdb-fallback branch from f61320b to 2cf5283 Compare August 27, 2026 19:10
@KhooLy

KhooLy commented Sep 5, 2026

Copy link
Copy Markdown
Member

Not desktop-specific please move to NuvioMobile.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Simkl entry with a season-specific IMDb ID cannot be opened - no fallback to its other known IDs

2 participants