Skip to content

fix(trakt): first connect survives network drops and shows the full history at once - #759

Merged
ProdigyV21 merged 24 commits into
ProdigyV21:mainfrom
ReichiMD:claude/vibrant-brahmagupta-1ns8ga
Sep 27, 2026
Merged

ProdigyV21 merged 24 commits into
ProdigyV21:mainfrom
ReichiMD:claude/vibrant-brahmagupta-1ns8ga

Conversation

@ReichiMD

Copy link
Copy Markdown
Contributor

Summary

Connecting Trakt for the first time went wrong in three places (reproduced on a phone with logcat, profile without a cloud account):

  1. The first attempt failed with a raw Unable to resolve host "api.trakt.tv" toast. While the user approves the code in a browser, ARVIO sits in the background and the phone briefly loses DNS. The polling loop treated any error without an HTTP code as fatal and ended the sign-in.
  2. The sync summary stayed empty for ~14 s. The silent full sync only started after the Continue Watching fetch (~10 s here).
  3. Home showed only part of the ticks until the app was restarted. Right after the token the watched cache loaded 576 movies / 4152 episode keys from Trakt. After the sync it was reloaded from TraktSyncService.getWatchedMovies/Episodes(), which without a Supabase account return what the last full sync kept in memory. When a show was rewatched, that sync expands only the 15 most recent shows (useProgressExpansion), so the cache shrank to 1170 episode keys and Trakt was skipped because the list was not empty: Home went from 151 started shows to 15. Home was not told about the reload either, because COMPLETED goes out before it.

Changes

  • Polling keeps going on network drops. isTransientTraktPollFailure: an IOException of the poll itself (DNS, timeout, dropped connection) no longer ends the activation. Only the poll is retried, not the work after the token is stored. Certificate failures are excluded because they do not heal by waiting. If the code runs out while the network stays down, the network error is reported instead of a plain expiry. A 409 right after such a failure (token issued, answer lost) reports that error instead of "code already used".
  • The sync starts before the Continue Watching fetch. performFullSync runs in its own job anyway, so the summary no longer waits for Continue Watching. On the device the summary was filled ~5 s after the token instead of ~14 s.
  • The watched cache reads Trakt whenever it is connected and merges it with the sync service's sets. With USE_NETLIFY_CLOUD_SYNC the Supabase reads are empty anyway, and without an account the in-memory sync result cannot stand in for the Trakt history.
  • Home is told when the watched cache was reloaded after an invalidation (TraktRepository.watchedCacheReloaded, a small WatchedCacheReloads helper) and runs refreshWatchedBadges(immediate = true). The first load after start or a profile switch is not reported, because Home runs its own pass for it.
  • Stale cache loads no longer win. A load overtaken by invalidateWatchedCache() or a profile switch used to mark the cache initialized with its old data. Now a generation counter keeps it from doing that, and a caller that waited for it loads the current state. The parallel sync made this more likely, and the reload event would have carried it to Home.

Notes, not changed here

  • The sync summary still shows the sync's own count (585 episodes here vs. 4152 on Trakt). It comes from the same 15-show expansion. The first sync of a Supabase account would have the same gap.
  • Without an account, the Trakt watched lists are now fetched again after each sync (the sync fetched them seconds before). That is one extra set of paged requests per sync. Reusing the sync's lists would be a larger change.
  • The background TraktSyncWorker refreshes the in-memory list without invalidating the cache. This is unchanged.
  • This touches HomeViewModel next to feat(home): show watched ticks without a Trakt sign-in #758, and the two merge without conflicts.

Testing

  • New TraktWatchedCacheLoadTest (Robolectric): Trakt is read even when the sync left a partial list in memory. A load overtaken by an invalidation does not keep its old data. Both fail on main.
  • New WatchedCacheReloadsTest: only a reload after an invalidation is reported, and a profile reset drops a pending one. TraktDeviceActivationTest: DNS, timeout and connect failures are transient; HTTP 404/410, missing credentials and certificate failures are not.
  • testSideloadDebugUnitTest: 1823 tests, 0 failures.
  • Test APK on a phone (fresh install, Trakt only, no cloud account), with temporary logcat lines on the test build only. Two DNS failures during polling, the sign-in still succeeded on the first attempt. Summary filled 4.9 s after the token. Watched cache after the sync: 576 movies / 4737 keys. Home ticked 11 cards right after the reload, without a restart.

created by Claude (Anthropic) on behalf of @ReichiMD

silentbil and others added 9 commits September 25, 2026 14:05
Home marked its cards only when a Trakt token was present, while Search,
Discover and Details already mark from the same watched cache for every
profile (local, Cloud, MDBList, SIMKL). Drop the Trakt gate, index the
episode history once into started show ids instead of scanning it per
show, and mark the latest UI state instead of writing back a snapshot
taken before the reads.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvQSEcH38SjW7hjUY8TvSs
The tick pass replaced the hero with the plain row card, dropping the
runtime, ratings, budget and network logo that are hydrated into the
hero only; it now takes over just the tick. A profile switch reset
every runtime state except the tick throttle, so the next profile could
wait up to 90 s for its ticks. The history index and the state update
run on Dispatchers.Default, as in Search, instead of the network pool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvQSEcH38SjW7hjUY8TvSs
On a device no tick ever showed: the startup cache rows were marked and
started the 90 s throttle, then the catalog load published fresh,
unmarked rows and its tick pass was throttled away; nothing asked again.
The throttle now only skips a pass when the rows are the very ones the
last pass marked. Every new set of rows (startup cache, catalog load,
next page, cloud reload) triggers a debounced pass, and Home resuming
(back from Details) forces one, as Search does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvQSEcH38SjW7hjUY8TvSs
The pass Home asks for on resume waited the full startup pause (3 s on
most phones, memoryClass <= 256) although the cache is loaded and the
pass takes milliseconds. Once a pass has run, a resume pass now only
debounces for 300 ms; the first pass after launch or a profile switch
keeps the startup pause.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvQSEcH38SjW7hjUY8TvSs
On a phone the ticks showed after the startup pass, vanished when the
catalog load re-published the rows (unmarked) and came back 3 s later.
New rows now take over what the last pass found at once; a full pass
still runs when no pass has run yet or on resume. The marking of rows
and hero moved into HomeUiState.withWatchedBadges, shared by both.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RvQSEcH38SjW7hjUY8TvSs
… history at once

- Device-code polling treated any error without an HTTP code as fatal, so a
  DNS or timeout blip while the user approves the code in a browser ended the
  sign-in with a raw exception text. Such IO failures of the poll itself are
  now retried until the code expires.
- The silent full sync after connecting is started before the Continue
  Watching fetch, so the sync summary no longer waits ~10 s for it.
- Without a cloud account the sync service only holds the last full sync in
  memory, which expands just the 15 most recent shows when a show was
  rewatched. The watched cache took that list instead of Trakt, so Home lost
  most series ticks until the app restarted. Trakt is now read unless the
  history comes from the cloud account.
- The watched cache reports a reload after an invalidation and Home marks its
  rows then: COMPLETED goes out before the reload, so Home read the old cache.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WAwV5pRZCPBFLm8WQ7FULi
…nected, guard stale cache loads

- Polls that fail without reaching Trakt are remembered: if the code runs out
  that way, the network error is reported instead of a plain expiry, and a 409
  right after such a failure (token issued, answer lost) no longer claims the
  code was reused. Certificate failures are not retried.
- The watched cache reads Trakt whenever it is connected. The Supabase reads
  are empty with USE_NETLIFY_CLOUD_SYNC, and without an account the sync
  service only holds the last (possibly partial) full sync, so the extra
  account check was not needed.
- A cache load overtaken by an invalidation or a profile switch no longer
  marks the cache initialized with its old data; a caller that waited for it
  loads the current state instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WAwV5pRZCPBFLm8WQ7FULi
@github-actions github-actions Bot added the area: android Changes to the Android app or Gradle build label Sep 26, 2026
claude and others added 15 commits September 26, 2026 09:39
Cards, the hero (TV and phone), the featured trailer card, the Home
backdrop preload and the details backdrop rewrite image.tmdb.org URLs to
the smallest official TMDB width that covers the slot instead of
'original' (1-2 MB per card). Stored URLs stay unchanged, other hosts
are untouched, and a URL is never rewritten to a larger size. A portrait
card without a poster is sized for the backdrop it shows.

The preload now uses the row's card width (TV 210 dp, phone 200 dp) and
Compose's rounding, so preload and card share one cache entry. The TV
details backdrop is sized like the Home hero, so both share one file.

Once startup has settled, the focused item's hero logo moves from
BACKGROUND to DEFERRED so it no longer waits behind the single-slot
card-logo fan-out; during startup the initial rows keep DEFERRED.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Qy4WX7LN7PngMAhsMQCJC8
…8974y

feat(home): show watched ticks without a Trakt sign-in
…eopened Sources

- A request Telegram did not answer in time now makes the whole lookup
  incomplete, so it is never cached as "no results" (the per-request 20s
  timeout used to look like an empty answer).
- When a phrasing's unfiltered first page has more results behind it, the
  phrasing is repeated with the video and document filters, so newer text or
  photo posts cannot push the files off the only page read. Pages with nothing
  behind them still cost one request.
- Telegram results found by a click-search are merged into every saved source
  list that Sources returns (fresh, stale, disk and addon-less paths), so
  reopening Sources keeps them, including with Telegram as the only provider.
- The phrase loop and the result cache moved into TelegramPhraseSearch /
  TelegramResultCache (no Android dependencies) with regression tests for a
  per-request timeout, a cut-off first page, and reopening after a
  click-search (including Telegram-only).
Subtitle auto-sync: whole-file reference (Matroska + MP4)
fix(telegram): search only on click, stop search floods and lost Telegram sources
…jan-5roog1

perf(home): load TMDB artwork at the size each slot draws
@github-actions github-actions Bot added the documentation Improvements or additions to documentation label Sep 27, 2026
@ProdigyV21

Copy link
Copy Markdown
Owner

Thanks for the Trakt reliability work. I kept the reconnect/history improvements and added serialized cache loading, stale profile/generation checks, independent provider fallback, and regression coverage. The combined batch passes 128 targeted unit tests and both Play/Sideload Kotlin compilation. Merging with your original commits and authorship preserved; live third-party account flows were not exercised.

@ProdigyV21
ProdigyV21 merged commit cf5a2b2 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 documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants