Skip to content

fix(trakt): load the watched cache before a local watched write - #763

Merged
ProdigyV21 merged 5 commits into
ProdigyV21:mainfrom
ReichiMD:claude/gesehen-liste
Sep 27, 2026
Merged

ProdigyV21 merged 5 commits into
ProdigyV21:mainfrom
ReichiMD:claude/gesehen-liste

Conversation

@ReichiMD

Copy link
Copy Markdown
Contributor

Summary

Marking a title watched or unwatched before the watched cache has loaded (the first seconds after launch or a profile switch) overwrote the profile's stored local watched history:

Action on an unloaded cache Stored local history before → after
mark film 4 watched 1, 2, 3 → 4
mark film 2 unwatched 1, 2, 3 + episodes → nothing (both keys removed)

A profile without Cloud sync cannot get that history back, and the shortened snapshot is also what the next cloud push exports (exportLocalWatched…ForProfiles).

Found while preparing the follow-up to #758; this is a separate fix in TraktRepository only.

Cause

Every local watched write updates the in-memory cache and then persists the whole cache as the snapshot (persistLocalWatchedSnapshotForCurrentProfile). Nothing made sure the cache was loaded first; ensureProfileCacheScope / a profile switch leave it empty until the next initializeWatchedCache.

Change

  • New loadWatchedCacheBeforeWrite(), called by all seven local writers before they touch the cache: markMovieWatched, markMovieUnwatched, markEpisodeWatched, markEpisodeWatchedWithoutTraktSync, markEpisodeUnwatched, markSeasonWatched, removeSeasonFromHistory. The player's auto-mark goes through markMovieWatched / markEpisodeWatched. It returns at once when the cache is loaded.
  • Calling initializeWatchedCache() alone is not enough for a write, for two reasons:
    • When it waits on another caller's load that gets cancelled, it returns with the cache still unloaded. At launch Home cancels and restarts its tick pass, including the load inside it, whenever rows land.
    • A load that overlaps a reset (profile switch, cloud restore → clearAllProfileCaches, invalidateWatchedCache) can finish with the history from before it.
  • The fix for both: a small reset counter, watchedCacheResets, bumped in clearProfileScopedMemoryCaches and invalidateWatchedCache. It lets the write detect either case and load again: up to three tries, then a final plain load.
  • No change to how the cache is loaded or read.

Price

  • The first write after launch or a profile switch waits for the load. That includes the Cloud/MDBList/SIMKL reads, and the Trakt history when the Cloud has none. The write already awaited the Cloud sync, so this adds one load, and only once.
  • If the caller is cancelled while that first load runs (e.g. leaving Details right away), the mark is dropped instead of half-written.

Testing

  • New TraktLocalWatchedSnapshotTest (Robolectric, same harness as TraktContinueWatchingRefreshTest), 6 tests:
    • film watched / film unwatched / episode / season on an unloaded cache keep the stored history
    • a write waiting on a load that gets cancelled keeps the history
    • a cloud restore during the write's load is not overwritten
  • Checked red/green: on main the film and episode tests fail ([4], and an unmark leaves nothing); with only a plain initializeWatchedCache() the last two still fail ([4], and [1, 2, 3, 4] instead of [4, 7, 8]).
  • testSideloadDebugUnitTest + compileSideloadDebugKotlin green locally; the test-APK run on the fork is green as well.
  • Device (phone, profile without Trakt/Cloud): marked three films, force-closed the app, reopened and marked a fourth right away from the Home long-press menu → all four keep their tick. The same steps in a build without this fix: only the fourth kept its tick, the other three were gone. (The test build also carried the phone menu fix from the long-press PR, so the entry was reachable.)

Not changed here (seen while reading)

  • cacheInitializing is a plain check-then-set, so two loads can still run in parallel. That was already the case and is only narrowed here, not fixed.
  • Cloud watched episodes come back under three keys (trakt: / show_trakt: / show_tmdb:), while an unmark removes only show_tmdb:. The snapshot grows, and an unmark from another device may not arrive locally. It needs a separate look.

created by Claude (Anthropic) on behalf of @ReichiMD

Every local watched write persists the whole in-memory watched cache as
the profile's snapshot. Right after launch or a profile switch the cache
can still be unloaded, so a mark made then replaced the stored history
with just that title - and an unmark removed the stored lists entirely.
A profile without Cloud sync cannot get that history back, and the
shortened snapshot is also what the next cloud push exports.

All seven writers (film, episode and season, watched and unwatched, and
the Supabase-only episode path) now load the cache first. The call
returns at once when the cache is already loaded; only the first write
after launch or a profile switch waits for the load.

initializeWatchedCache alone is not enough for a write: when it waits on
another caller's load that gets cancelled (Home restarts its tick pass
while rows land at launch) it returns unloaded, and a load that overlaps
a profile switch or cloud restore can finish with the history from
before it. A reset counter lets the write detect both and load again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NtiKMWmfLDYKRRb5CBptwd
@github-actions github-actions Bot added the area: android Changes to the Android app or Gradle build label Sep 26, 2026
@ProdigyV21

Copy link
Copy Markdown
Owner

Thanks, this fixes a real history-loss risk. I integrated it with #759 and made the pre-write load local-only, so marking a title does not wait for a full network sync. Snapshot writes merge transactionally, preserve concurrent changes, and reject a changed profile/cache scope. Added coverage for cancellation, cloud restore, concurrent marks, and stale remote history after an unwatch. All eight snapshot tests pass within the 128-test batch; both Android variants compile. Original author commits are preserved.

@ProdigyV21
ProdigyV21 merged commit 79047e6 into ProdigyV21:main Sep 27, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: android Changes to the Android app or Gradle build

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants