Skip to content

fix: sync follow-ups: episode reviews, TV entries and rate limits - #156

Merged
builtbyproxy merged 11 commits into
mainfrom
audit/stack-09-sync-followups
Oct 8, 2026
Merged

builtbyproxy merged 11 commits into
mainfrom
audit/stack-09-sync-followups

Conversation

@builtbyproxy

@builtbyproxy builtbyproxy commented Oct 8, 2026 •

Copy link
Copy Markdown
Owner

Layer 11 of the audit fix stack (releases 2.11.1). Fixes audit findings COR-16, COR-17 and REL-15, plus two smaller items found while building earlier layers (the History page cap and the telemetry spec's start window). Stacked on #154.

COR-17, checked against live markup: on letterboxd.com today a TV entry Letterboxd lists as a film (for example Chernobyl) has data-tmdb-type="movie" and an empty data-tmdb-id, and only its TMDb button links to themoviedb.org/tv/. So the collision the audit predicted does not happen with today's markup (the empty id is never mapped). This PR hardens the reader anyway: the TMDb button decides, and the tests model the real markup of both kinds of page.

Release notes

Jellyscribe is steadier when Letterboxd or Serializd ask it to slow down. A rate-limit pause is honoured for up to a minute. Anything longer skips the film or episode until the next sync, and a Letterboxd sync that keeps hitting the limit stops early instead of trying every film. Letterboxd retries are now accepted instead of refused as a repeat. Stopping a sync, or restarting Jellyfin, no longer waits out a long pause. When you sign in with your Letterboxd website login, a TV series that Letterboxd lists as a film can no longer be mistaken for a movie with the same TMDb number, so the wrong film is never marked watched or rated. A Serializd review of an episode now lands on the same episode a log would: for a show Serializd lists as a single season, it goes to the right episode of that season, and it is refused if Serializd has no such episode. The activity lists on both dashboards show the full page they ask for again.

What's broken

  • Long rate limits. When Letterboxd or Serializd answer 429 with a long Retry-After, the sync sleeps for the whole of it, however long that is. Stopping the task or the server cannot interrupt that sleep, or any other retry backoff.
  • The Letterboxd retry. It resends the exact signed URL, with the same nonce and timestamp, which the API can refuse as a replay.
  • TV entries on the website path. With the Letterboxd website login, a TV series that Letterboxd lists as a film resolves to its TMDb TV id. That number can be an unrelated movie's id, which then gets marked watched or rated.
  • Serializd episode reviews. For a show Serializd lists as a single season, an episode review fails or lands on the wrong episode, even though logging that episode works.
  • History page size. GET History caps a page at 200 rows while both dashboards ask for 250.
  • Telemetry spec. It describes a plus or minus 6 hour jitter that the code does not have.

Why it happens

  • Waits. Both API clients waited Retry-After with no upper bound and no cancellation token. No service method the runners call took a token.
  • Signing. The Letterboxd retry reused the url that already carried the first attempt's signature.
  • TV entries. The website-path resolver read the body's data-tmdb-id without its data-tmdb-type. The API path already skipped /tv/ links.
  • Episode reviews. They resolved the season by number directly and never went through SerializdSeasonFallback.

What this PR does

  • Rate limits.
    • A 429 is waited out at most once, for up to 60 s (RetryAfterLimit, which reads seconds or a date).
    • A longer one fails the item. On Letterboxd it counts as a block, so a run stops after three.
    • The Letterboxd retry is signed again with a fresh nonce and timestamp.
  • Cancellation.
    • Adds optional CancellationToken parameters to the service methods the runners call, and every scheduled runner and the rating handler pass theirs.
    • The token cancels waits, backoffs, the Serializd request gate and reads, never a write already sent.
    • Catches around these calls let a shutdown cancellation through.
  • TV entries.
    • The scraper reads data-tmdb-type (or, failing that, the TMDb button's /tv/ link) and skips TV entries.
    • It caches them so they are not fetched again.
    • A TMDb-id lookup that resolves to a TV entry reports not found.
  • Episode reviews.
    • They resolve their target through SerializdSeasonFallback, with season lengths from the caller's own library.
    • They are refused when the episode would land past Serializd's season.
  • History and docs.
    • GET History serves up to 250 rows.
    • The telemetry spec describes the 0 to 719 minute start window.
    • CLAUDE.md notes the cap and the cancellation rule.

How it was tested

  • New xUnit tests:
    • the Retry-After cap and date handling;
    • the fresh signature and nonce on retry;
    • long 429s failing at once, and counting as a block;
    • cancellation of the 429 wait, the 5xx backoff, the Cloudflare backoff and a Serializd read;
    • TV pages on both scraper paths, plus the cache marker;
    • single-season episode reviews, including the refusal;
    • History against the page sizes the dashboards request.
  • CancelledRun_RecordsNoFailure now matches the run's own token.
  • I reverted each production change and confirmed its test fails.
  • Full suite: 1343 passed, 0 failed.
  • Security, ai-smells and performance review gates passed with fixes, all applied or answered above.

@builtbyproxy
builtbyproxy added this pull request to stack #150 October 8, 2026 00:16
@builtbyproxy builtbyproxy changed the title audit/stack 09 sync followups fix: sync follow-ups: episode reviews, TV entries and rate limits Oct 8, 2026
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from 0985b6f to 05467b9 Compare October 8, 2026 02:15
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch 3 times, most recently from 30615ee to eaa9860 Compare October 8, 2026 02:22
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch 2 times, most recently from a5f7767 to 511d37e Compare October 8, 2026 02:26
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from 511d37e to 941b623 Compare October 8, 2026 02:28
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from 941b623 to adc3a14 Compare October 8, 2026 02:30
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch 2 times, most recently from 3db59c2 to c74f32a Compare October 8, 2026 02:33
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch 2 times, most recently from 7b8b769 to 5ee4e9d Compare October 8, 2026 02:39
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch 2 times, most recently from cb77156 to c3b9b26 Compare October 8, 2026 02:44
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from c3b9b26 to 629a7ae Compare October 8, 2026 02:47
Base automatically changed from audit/stack-08-privacy-previews to main October 8, 2026 02:50
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from 629a7ae to 9fb091f Compare October 8, 2026 02:50
…and let a stopped sync cancel its network waits
…including shows Serializd lists as one season
@builtbyproxy
builtbyproxy force-pushed the audit/stack-09-sync-followups branch from 9fb091f to 4aa6b51 Compare October 8, 2026 02:53
@builtbyproxy
builtbyproxy merged commit 408f66e into main Oct 8, 2026
4 checks passed
@builtbyproxy
builtbyproxy deleted the audit/stack-09-sync-followups branch October 8, 2026 03:31
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.

1 participant