Skip to content

feat(web): browse downloads without the server - #226

Open
mia-riezebos wants to merge 3 commits into
feat/178-offline-image-subsfrom
feat/179-offline-browse
Open

mia-riezebos wants to merge 3 commits into
feat/178-offline-image-subsfrom
feat/179-offline-browse

Conversation

@mia-riezebos

@mia-riezebos mia-riezebos commented Oct 21, 2026 •

Copy link
Copy Markdown
Member

With the server unreachable, the web app showed only "Server unreachable": the Downloads page, and the pages of what you downloaded, all needed the API. This is #179 of PRD #173.

A download now saves the Item's detail and artwork next to its file, and the app reads them when the server can't answer.

  • Saved at download time. The download record keeps the Item detail the page showed. The transfer saves the Item's poster, backdrop, logo and thumb, plus its Show's, under downloads/artwork/ in OPFS, once per artwork id. Deleting the last download that draws an artwork deletes it, under the index lock, so two Episodes of one Show share their Show's poster safely.
  • Artwork offline. The service worker answers /api/artwork/* from the network, and from the saved copy when the network fails or a proxy answers 5xx. Every artwork component works unchanged. It still caches no API responses.
  • Shell offline. The session and the libraries list are kept in localStorage (thalia.session) after each successful load, and stand in when the server is unreachable. Sign out forgets them. /play gets the same fallback, since it shares loadSession.
  • Downloads page. Movies first, then each Show under its poster with a panel per Season (Specials last) and Episodes in order. Each row links to its page and keeps its play, pause, resume, cancel or delete buttons.
  • Detail pages offline. A finished download's page renders from its saved detail with Play and the download state. Favourite, rating, the admin menu, Store request and Access are hidden. Any other page says "Not available offline" with a Go to Downloads button.
  • Fix found on the way. A tab closed right after a download's last byte left its index entry at "downloading", so it came back as paused. Restore now treats a whole file on disk as finished.

Closes #179

Stacked on #239

This PR now stacks on #239 (offline playback, #225 → #233 → #237 → #239), so the two offline stacks are one. Play on an offline page opens the download in that player (/play/{itemId}?version={downloadedVersionId}&offline), the same link as Play on the Downloads page. A download record keeps both its saved detail (this PR) and its saved subtitle files (#233).

Screenshots

The fixture artwork is flat blue on purpose. Before: the downloads list from #208 (1440 light); offline, every page here showed the full-screen "Server unreachable".

Downloads, server stopped:

Light Dark
1440 downloads 1440 light downloads 1440 dark
390 downloads 390 light downloads 390 dark

Detail pages, server stopped:

Downloaded movie, 1440 Downloaded episode, 390 dark Not downloaded, 390
arrival offline episode offline not downloaded

Tests

  • apps/server/src/api/offline-browse-browser.test.ts (new, headless Chromium): downloads a movie and two Episodes through the Download button, stops the server, then checks that Downloads lists them grouped as Movies / Severance / Season 1 in episode order with both posters loaded from OPFS, that the movie's page shows its title and its Play link plays the download with the server still down, that a page that isn't downloaded says "Not available offline", and that deleting the movie removes its artwork and keeps the Show's. 3 passes in a row, alongside downloads-browser.test.ts.
  • lib/downloads/library.test.ts (new): grouping order, Specials last, legacy records last; an offline detail lists only the downloaded Version, and only for finished downloads.
  • lib/downloads/store.test.ts: artwork is saved once per id and deleted with the last download that draws it; restore finishes a record whose whole file is on disk.
  • Web bun test: 301 pass on the joined stack. bun run check: 0 errors, 0 warnings. bun run lint: 0 errors, the 9 CSS warnings already on main.

Made with claude-opus-5.5 using Claude Code in T3 Code.

🤖 Generated with Claude Code

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 60101147-b5b2-4e28-b296-b93a9451e177

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Important

Interrupted artwork saves can leave empty files that subsequent downloads permanently reuse. Fix the artwork reuse check before merging.

Reviewed changes across offline session fallback, download metadata and artwork persistence, service-worker responses, and the Downloads/detail UI.

  • Saved download data: Download records retain Item details and shared artwork, with cleanup when the final referencing download is deleted.
  • Offline shell and detail: Cached session/library data opens the shell without the server; completed downloads supply detail pages with server-only actions hidden.
  • Downloads browsing: Movies and Shows render with artwork, Season grouping, Episode ordering, detail links, and transfer controls.
  • Verification: All 288 web unit tests pass and Svelte checks report no errors or warnings. A temporary regression test reproduced the empty-artwork reuse bug and was removed afterward; the new browser integration test was reviewed but not rerun. Offline playback is explicitly deferred to #175.

Pullfrog  | Fix all ➔ | Fix 👍s ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

Comment on lines +264 to +265
await folder.getFileHandle(id);
continue;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Retry artwork files left empty by an interrupted save

Creating a file handle with { create: true } creates an empty entry before the writable commits its bytes, so a failed write or tab termination can leave a zero-byte file behind. This existence-only check skips that file on every subsequent attempt, leaving later sibling Episode downloads with broken artwork offline even when they finish successfully.

Technical details
## Affected sites
- `saveArtwork` checks only handle existence before skipping a saved copy.
- The later `getFileHandle(id, { create: true })` creates the entry before `createWritable()`/`write()`/`close()` completes.
- `savedArtwork` and the service worker accept this empty file as the offline response.

## Required outcome
- Reuse only a successfully committed, non-empty artwork file; an empty placeholder must remain eligible for saving.
- Add a regression test that creates an artwork handle without committing content, completes a download referencing it, and verifies that the artwork is fetched and saved.

## Reproduction
Using the existing `FakeDir`, precreate `downloads/artwork/poster` with `getFileHandle("poster", { create: true })`, then start a download whose detail references that poster. The media download reaches `complete`, but the artwork fetch list stays empty and the poster remains zero bytes. A temporary test asserting the expected artwork fetch failed on this head.

The file-creation site precedes the commit. MDN documents that getFileHandle creates a missing file and that createWritable changes are not reflected until close.

@pullfrog

pullfrog Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

The failed CI job has one failure: keyframe-index jobs > a slow read under a short lease completes through the worker at apps/server/src/libraries/jobs.test.ts:1320, reporting Job lease expired before its holder finished. The test uses a 300 ms lease, and neither it nor the worker/queue implementation is changed by this PR; the new offline-browse browser test and all 288 web tests passed, so this appears to be an unrelated timing-sensitive failure. Per the CI-fix workflow, I stopped without changing or pushing unrelated code; the working tree is clean, and rerunning the failed CI job is the appropriate next step.

Task list (1/3 completed)
  • Inspect PR branch and CI logs; confirm the failure was introduced by the PR
  • Apply the smallest fix and rerun the failing CI commands (cancelled: failure is outside the PR's changed code)
  • Review and push the fix to PR feat(web): browse downloads without the server #226; confirm a clean working tree (cancelled: no applicable fix; working tree is clean)

Pullfrog  | View workflow run | via Pullfrog | Using gpt-6.1-sol | 𝕏

@mia-riezebos
mia-riezebos force-pushed the feat/179-offline-browse branch from 612ea60 to 434f40c Compare October 21, 2026 08:07
@mia-riezebos
mia-riezebos changed the base branch from main to feat/178-offline-image-subs October 21, 2026 08:07

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues found in this delta. The earlier interrupted-artwork-save finding remains unresolved, so this is not an approval.

Reviewed changes since the previous review, including the rebase onto feat/178-offline-image-subs and the offline playback links.

  • Connected offline Play: Added &offline to the detail page's primary and per-Version Play links so they open the downloaded file rather than an online session.
  • Preserved download integration: Kept stored subtitle data, subtitle cleanup, and codec-gated Downloads playback controls alongside the offline browsing changes on the updated base.
  • Extended browser coverage: Added a check that the offline detail page's Play link loads video while the server is stopped.

Verification: all 301 web unit tests pass; Svelte diagnostics report 0 errors and 0 warnings after bun install --frozen-lockfile. The browser integration test was reviewed but not rerun because ffmpeg and ffprobe are unavailable in this environment.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6.1-sol | 𝕏

@pullfrog

pullfrog Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Pushed signed commit e8754da to PR #226: the offline-browse test now waits for artwork cleanup, and browser polling awaits asynchronous predicates instead of treating unresolved Promises as success. Reproduced the exact assertion failure under index-lock contention and verified the fix; ten consecutive browse runs, download-resume coverage, lint, type checks, build, and bun run test passed (database/S3 suites were skipped in the root run; affected browser tests ran separately against local Postgres). The separate subtitle-menu timeout did not reproduce and its test passed locally; the working tree is clean.

Task list (3/3 completed)
  • Inspect PR branch and CI logs; confirm the failure was introduced by this PR
  • Reproduce the failure and implement the smallest fix
  • Run CI verification, review the diff, and push a signed commit to the PR

Pullfrog  | View workflow run | via Pullfrog | Using gpt-6.1-sol | 𝕏

mia-riezebos and others added 3 commits October 21, 2026 12:11
A download now saves its Item's detail and artwork next to the file, so
the Downloads page and the pages of downloaded Movies and Episodes open
with the server stopped.

- The download record keeps the Item detail; the transfer saves its
  poster, backdrop, logo and thumb (and its Show's) under
  downloads/artwork. Deleting the last download that draws an artwork
  deletes it, under the index lock.
- The service worker answers /api/artwork from those copies when the
  server can't.
- The shell keeps the last signed-in caller and libraries, so it opens
  offline; sign out forgets them.
- Downloads groups Movies, then Shows by Season and Episode, with
  artwork, each row linking to its page.
- Offline, a downloaded Item's page shows its saved detail with Play;
  any other says it needs the connection.
- Restore treats a record whose whole file is on disk as finished, so a
  tab closed right after the last byte no longer shows it paused.

Closes #179

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Play on a downloaded Item's offline page opened the server's player,
which said the server was unreachable. It now opens the download.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@mia-riezebos
mia-riezebos force-pushed the feat/178-offline-image-subs branch from 47f1c4e to ed0090b Compare October 21, 2026 10:58
@mia-riezebos
mia-riezebos force-pushed the feat/179-offline-browse branch from e8754da to 9f9b55a Compare October 21, 2026 10:58

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ℹ️ No new issues found in this delta. The earlier interrupted-artwork-save finding remains unresolved, so this is not an approval.

Reviewed changes since the prior review: the two feature commits replay unchanged on the updated base; the new commit fixes browser-test synchronization.

  • Updated asynchronous polling: Changed Page.waitFor to await a condition's resolved value before testing its truthiness, preserving synchronous conditions and retrying rejected evaluations.
  • Waited for artwork cleanup: Added an OPFS condition after deleting the Movie so the test waits for its artwork to disappear while the Show poster remains, rather than treating row removal as cleanup completion.

Verification: all 32 download tests passed. Direct checks of the actual helper in Chrome passed for synchronous conditions, asynchronous false-to-true polling, rejection retries, and timeouts. The full offline-browser integration test was not rerun because ffmpeg and ffprobe are unavailable.

Pullfrog  | Fix it ➔ | View workflow run | Using gpt-6-astra | 𝕏

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.

Offline: browse downloads without the server

1 participant