Commit graph

5 commits

Author SHA1 Message Date
mithral
4c51482dd6 Home: five sections — Continue, Most Viewed, Suggestions, Recently Added, Explore
The landing screen now reads as a product rather than a list.

- migration 0006: item_views, per user. "Most viewed" is personal — what I
  re-read is not what you re-read. Counted when an item is opened in a reader,
  so the number means consumed, not merely fetched.
- Suggestions: unstarted items related to what the user has been consuming —
  same author first, then same kind, then recent as a floor. Each carries the
  reason it was chosen ("More by Daft Punk"), because a suggestion nobody can
  understand is noise.
- Explore: a mixed sample across every kind, excluding in-progress items so the
  zone is discovery rather than a repeat.
- Home fetches all five shelves in one request; the screen never pops in.
- web: Shelf gained subtitle/badge support, Card gained a subtitle, and the
  home renders four shelves plus a wrapping Explore grid.

Three bugs found and fixed while verifying:

- Continue showed a multi-track audiobook once per chapter. GROUP BY item_id
  with MAX(updated_at) picks one card per item (SQLite takes the bare columns
  from the row that produced the max).
- Suggestions and Explore passed their SQL arguments in the wrong order: the IN
  list precedes the user id in those queries, so a user id was being bound as a
  library id. Covered by a scoping test that would have caught it.
- The sidebar lost its media types on a cold, logged-out load. bootstrap()
  fetched media types inside a Promise.all with /auth/me, so the expected 401
  rejected the whole thing and nothing refetched after login. Media types are
  public and now load independently.
2026-10-10 16:59:39 -04:00
mithral
dc95a9f78a Redesign the user-facing experience: home, browse, item pages
The app looked like an admin panel. This is the product people actually see
when they open their library.

- Home: a landing screen with a Continue shelf (progress bars, straight from
  the progress table) and per-type shelves of recent additions. One request
  feeds it, so it does not pop in piece by piece.
- Browse: cover grid per media type with type chips, plus browse-all. Powered
  by a new GET /items that lists across every visible library, filtered by
  kind, through the same access chokepoint.
- Item page: cover, kind, title, authors, progress, a primary action that says
  Continue when you have started, description, and the file list for
  multi-file items. Cards now open the item page rather than jumping straight
  into a reader.
- Search: results grouped by media type, cover-forward.
- Navigation moved from "libraries" to "media types" — you browse Ebooks, not
  "the Ebooks folder". Libraries became an admin concern.
- Design system: dark by default, cover-forward, one accent, tighter type
  scale, horizontal shelves, skeleton loaders.
- media: GET /home, GET /items, and authors on item detail.

Verified by screenshot in a live instance: home shelves, browse grid, and item
page all render correctly.
2026-10-09 21:38:11 -04:00
mithral
1b11867efe Phase 6a: directory grouping, audiobooks and music playback
A book is a folder of chapters, not a file. The scanner gained a grouping
mode so a directory of tracks becomes one item with ordered files.

- registry: MediaType.Group (file | directory). Audiobooks and music group by
  directory; everything else stays one item per file.
- scanner: builds groups, upserts one item per group, marks items missing when
  their directory disappears, and keeps the file-level added/updated/missing
  accounting. Tracks are ordered naturally, so chapter 2 precedes chapter 10.
- natsort: one shared natural-order comparator, now used by both the scanner
  and the comics module (which had its own copy).
- media: GET /items/{id}/files/{fileID} streams one specific track. It
  verifies the file belongs to the item, so an item id cannot be paired with
  another item's file id.

No new module was needed for playback: the polymorphic progress table already
keys on file_id, so per-chapter resume fell out of the existing design. The
web player resumes the exact track and offset.

Verified: 3 chapters grouped into one book titled "Mistborn", natural order,
range request returned 206, mismatched file id 404s, per-track progress
round-trips.
2026-10-09 21:20:47 -04:00
mithral
c3b7db7114 Phase 4: CBZ comics module and reader
- comics: reads pages straight out of the archive on demand. No extraction
  pass, so a 200-page CBZ is not materialised for a reader who views three
  pages. Pages are read into memory and served via http.ServeContent, which
  gives Range for free.
- natural page ordering: page2 sorts before page10. Lexical ordering shuffles
  a numbered archive, which is the difference between a readable book and a
  scrambled one.
- skips __MACOSX and dotfiles; rejects archives with no images.
- cover is the first page.
- media.Item gained ModTime (from the backing file) so page responses carry a
  Last-Modified and support conditional GETs.
- web: ComicReader with a bounded prefetch window (two pages each way), page
  jump, keyboard nav, and progress stored as a page number.

Verified: 3-page CBZ served in natural order, out-of-range 404, cover from
page 1, progress round-trip. svelte-check 0/0.
2026-10-09 21:07:59 -04:00
mithral
5c63932419 Phase 3: EPUB module, file streaming, per-user progress
- stream: serving goes through http.ServeContent, so Range/If-Range/
  If-Modified-Since are correct for every media type by construction
- progress: the polymorphic store. One row per user/item/file; the PK includes
  the file so multi-track audiobooks resume per track later. Finished is
  inferred when position reaches duration.
- media: generic item endpoints (detail, file, cover, progress, recent). Every
  one routes through auth.CanAccessLibrary; an unauthorised item 404s.
- covers: a per-kind extractor registry. Extraction is lazy and recorded in
  the assets table. A missing cover is a placeholder, never a 500.
- modules/ebooks: EPUB parsing (container.xml -> OPF -> metadata, spine, cover
  with three fallback strategies), cover extraction to /config, and lazy
  metadata parsing so parser fixes need no rescan.

Verified against a generated EPUB: title/2 authors/language/publisher/spine,
cover served as PNG, range request returned 206 with correct Content-Range,
progress round-tripped.
2026-10-09 16:36:27 -04:00