# Spec: Downloads as a browsable offline library

**Status:** Draft — ready to implement
**Scope:** Frontend-heavy; one new repository-client browse path. Minimal Rust.
**Requirements:** UR-055 → DR-081, DR-082, DR-083, DR-084; UR-056 → DR-085
(see [requirements.md](../requirements.md)).
**UX spec:** [ux-flows.md §7.2–7.7](../ux-flows.md).

## Summary

Replace the flat Active/Completed download list with two views under
`/downloads`:

1. **Downloaded** (default) — the library, filtered to what's on the device,
   using the *same* browse screens as online (grids, cards, detail pages).
2. **Transfers** — the existing progress-row list, demoted to a secondary tab,
   showing only in-flight transfers.

Plus per-item disk usage (UR-056) shown in familiar units on cards, detail
pages, a device total, and the remove confirmation.

## Motivation

A user who downloaded three seasons and two albums sees ~70 individual transfer
rows today, with no grouping and no reuse of the library UI. "What do I have
offline" and "what is downloading" are different questions crammed into one flat
list. Browsing offline should feel exactly like browsing online.

## Background: verified current state

1. **The offline repository is already a browsable tree.**
   [offline.rs](../../src-tauri/src/repository/offline.rs) — `get_items` returns
   downloaded items **plus** containers (MusicAlbum, Series, Season) that have at
   least one downloaded child. `get_libraries`, `get_item`, and `search` all
   filter to downloaded content via CTEs. This is the data source for
   Downloaded; **do not build a new query layer.**

2. **The client cannot reach it independently.**
   [repository-client.ts](../../src/lib/api/repository-client.ts) `getItems` →
   `repositoryGetItems` always goes through the **hybrid** repository
   ([hybrid.rs](../../src-tauri/src/repository/hybrid.rs)), which merges cache and
   server. There is no "offline only" browse path exposed. This is the one real
   backend gap (DR-082).

3. **Downloads page is a flat two-tab list.**
   [downloads/+page.svelte](../../src/routes/downloads/+page.svelte) — Active /
   Completed tabs, one `DownloadItem` row per transfer, no browsing.

4. **Library browse components are reusable as-is.** `LibraryGrid`, `MediaCard`,
   the `/library/[id]` detail page (§5A/§5B) render whatever items they are
   given. Downloaded browse is those components with an offline-scoped source.

5. **A related fallthrough bug is already tracked** (DR-080, another session):
   `HybridRepository::get_items` treats an empty offline result as a cache miss
   and falls through to the server. The offline-only browse path (DR-082) must
   **not** share that behaviour — an empty result there is authoritative "nothing
   downloaded here."

6. **Concurrency, the 3-download cap, and the auto-pump are backend concerns.**
   Do not surface them as manual controls; do not loop `startDownload` from the
   frontend (see [CLAUDE.md](../../CLAUDE.md) gotchas).

## Design

### View split (DR-081)

`/downloads` renders a **Downloaded** / **Transfers** switch. Downloaded is the
default. Transfers shows a count/badge only while transfers are active.
Initiating downloads stays on item/album/series detail pages (§7.1) — this page
does not start downloads.

### Offline-scoped browse source (DR-082, DR-083)

Add an explicit offline-only browse path so Downloaded never merges server
results and never depends on reachability. Two viable shapes — pick per the
codebase, do not do both:

- **(a)** A dedicated command (e.g. `repository_get_downloaded_items` /
  `_libraries`) that calls the offline repository directly, with a matching
  client method; or
- **(b)** An explicit `offlineOnly`/scope flag on the existing get-items path
  that bypasses the hybrid merge and the empty→fallthrough behaviour.

Either way: an empty result is authoritative (do **not** reuse the DR-080
fallthrough), and the path is available while the server is reachable (a user
online still wants to browse their downloads).

Downloaded then reuses `LibraryGrid` / `MediaCard` / the detail page against this
source. Omit libraries and containers with no downloaded content. Badge
partially- vs fully-downloaded containers. Play uses the local file; remove is
available at item / album / season / series level and removes a container from
the browse when its last downloaded child goes.

### Transfers view (DR-084)

The existing list, filtered to in-flight rows only: downloading (with progress),
queued, paused, failed, waiting-for-WiFi (the DR-074 state from the other
session). Controls: Pause / Resume / Cancel / Retry. Completed transfers leave
this view — they appear in Downloaded. Empty state points at the library.

### Disk usage (DR-085, UR-056)

- **Source the bytes from the download manager** — it writes the files and can
  stat them. Aggregate to album/season/series subtotals and a device total.
  This is display + aggregation, **not** new tracking.
- **Format once, consistently.** One shared formatter, human units, 2–3
  significant figures (`1.2 GB`, `340 MB`). Binary vs decimal — pick one and use
  it everywhere.
- **Surface it in familiar places:** a secondary size label on the card and
  detail page; a device total at the top of Downloaded (`3.4 GB · 12 items`)
  that reconciles with the listed sum; a reclaim figure in the remove
  confirmation ("frees 1.2 GB"). No separate "storage report" screen.
- Sort/filter by size is a nice-to-have, not required for v1.

## Out of scope

- Changing download initiation, the 3-concurrent cap, or the auto-pump.
- The catalog-browse / show-server-catalog toggle (UR-052, another session) —
  that governs the *online offline-fallback* library; this is the dedicated
  Downloads surface. They should be consistent but are separate work.
- Fixing the DR-080 hybrid fallthrough bug (owned elsewhere) — just don't depend
  on that behaviour here.

## Acceptance criteria

- [ ] `/downloads` opens on Downloaded and can switch to Transfers.
- [ ] Downloaded lists only libraries/containers with downloaded content, using
      the same grids/cards/detail pages as online browsing.
- [ ] Browsing Downloaded never shows non-downloaded server items, online or off.
- [ ] An empty Downloaded result reads as "nothing downloaded," never falls
      through to the server.
- [ ] Play from Downloaded plays the local file.
- [ ] Remove works at item/album/season/series level and updates the browse.
- [ ] Transfers shows only in-flight rows with working controls; finished
      transfers move to Downloaded.
- [ ] Each downloaded item/container shows its on-disk size; a device total is
      shown and reconciles with the sum; remove states the reclaim amount.
- [ ] `bun run check`, `bun run test`, and (if Rust touched) `cargo test` +
      `cargo clippy` pass.

## Testing

- Repository client: the offline-only browse path returns downloaded content and
  its containers, and an empty result does **not** trigger server fallthrough.
- Downloaded view: libraries/containers with no downloads are omitted;
  partial/full container badging.
- Transfers: only in-flight statuses render; a completed transfer disappears.
- Size formatter: rounding and unit thresholds; subtotal aggregation; device
  total reconciles with listed items.
- If a Rust command is added, add the tauri IPC param-naming coverage per
  [CLAUDE.md](../../CLAUDE.md) (camelCase rule).

New requirement-implementing code needs `TRACES:` comments. Suggested tags:
view split `UR-055 | DR-081`; offline browse path `UR-055 | DR-082, DR-083`;
Transfers `UR-055 | DR-084`; size display `UR-056 | DR-085`.

## Notes for the implementer

- Read [ux-flows.md §7.2–7.7](../ux-flows.md) first — behavioural spec; this is
  the implementation plan.
- The offline repository already does the hard part. The main work is a clean
  offline-only client path and reusing the library components — resist
  rebuilding browse UI.
- Another session is active in downloads/offline/connectivity code (DR-074,
  DR-078–080). Coordinate on [downloads/+page.svelte](../../src/routes/downloads/+page.svelte)
  and the repository layer; check `git diff` before repairing unexpected changes.
