feat(content): serve the mounted content folder under /content (spec 003)
Audio with range requests, library.json, Homer books; LRC parser. Content stays out of git and the image. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
9d063fcbe0
commit
27b1d2fe26
15 files changed
+527
-10
No files matched your search
@@ -11,7 +11,7 @@ shared credits snippet.
|
||||
## Map
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/server.ts` | `startServer(port)`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), 404 otherwise. Runs when `import.meta.main`; `PORT` env, default 3000. |
|
||||
| `src/server.ts` | `startServer(port, { contentDir? })`: routes `/`, `/mockups/NN-slug/` (+ 302 from the no-slash form), `/content/*` via `src/content/routes.ts`, 404 otherwise. Runs when `import.meta.main`; `PORT` env, default 3000. |
|
||||
| `src/index.html`, `src/index.ts`, `src/index.css` | Index page: static cards linking each mockup, then `mountCredits()`. |
|
||||
| `src/mockups/registry.ts` | `mockups: MockupInfo[]` — slug, number, name, pitch, what to try. |
|
||||
| `src/mockups/NN-slug/` | One design direction: `index.html` (links `../../shared/credits.css`, has `<footer data-fan-credits>`), `main.ts`, `style.css`, plus optional helper modules (e.g. `scene.ts`, `layout.ts`). |
|
||||
@@ -22,7 +22,11 @@ shared credits snippet.
|
||||
| `src/shared/audio-slot.ts` | `createAudioSlot(song)`: `<audio>` for `song.audio`, else an empty-state slot. |
|
||||
| `src/shared/video-cards.ts` | `createVideoCards(song)`: YouTube link cards, no embeds. |
|
||||
| `src/shared/credits.css` | Styles for credits, audio slot, video cards. |
|
||||
| `tests/` | `data.test.ts` (dataset invariants), `shared.test.ts` (DOM modules under happy-dom), `server.test.ts` (every route 200s, index links every registry slug, each mockup has the credits hook). |
|
||||
| `src/content/routes.ts` | `contentResponse(req, dir)`: `/content/library.json`, `/content/files/<path>` (traversal-guarded, HTTP ranges), `/content/homer/<odyssey\|iliad>/<book>.json`. All 404 when no folder is mounted. |
|
||||
| `src/content/library.ts` | `scanLibrary(dir)`: sagas (story order) and songs from `Songs and Lyrics/EPIC - <Saga> (<year>)/`; `.lrc` preferred over `.txt`. |
|
||||
| `src/content/homer.ts` | `splitBooks(text)`: Gutenberg Butler text → books (heading, summary, paragraphs). |
|
||||
| `src/shared/lrc.ts` | `parseLrc(text)`: timed lyric lines, sorted. |
|
||||
| `tests/` | `data.test.ts` (dataset invariants), `shared.test.ts` (DOM modules under happy-dom), `server.test.ts` (every route 200s, index links every registry slug, each mockup has the credits hook), `content.test.ts` (LRC, library scan, Homer split, `/content/*` routes on a temp fixture). |
|
||||
| `Dockerfile` | Two-stage `oven/bun:1.3`: install + build, then copy `dist/` and run from `WORKDIR /app/dist`. |
|
||||
|
||||
## Data flow
|
||||
@@ -32,7 +36,9 @@ shared credits snippet.
|
||||
3. `main.ts` renders the direction and calls `mountCredits()` into `<footer data-fan-credits>`.
|
||||
|
||||
## External dependencies
|
||||
None at runtime. Fonts (`@fontsource/*`) and three.js are bundled from npm. Outbound links only: YouTube
|
||||
Optional at runtime: the content folder named by `CONTENT_DIR` (real audio, lyrics, cover art, Butler's
|
||||
Odyssey and Iliad). It is mounted read-only, never committed or baked into the image (spec 003). Without it
|
||||
the site works and `/content/*` answers 404. Otherwise none. Fonts (`@fontsource/*`) and three.js are bundled from npm. Outbound links only: YouTube
|
||||
(video cards) and Project Gutenberg (Odyssey references) — the site works without them.
|
||||
|
||||
## Invariants
|
||||
|
||||
@@ -66,3 +66,11 @@ persistent side panel. No new dependency.
|
||||
## 2026-10-30 — three.js allowed in mockup 16
|
||||
|
||||
**Decision:** mockup 16 (Voyage, readable) may use three.js; it iterates on 15. No new dependency.
|
||||
|
||||
## 2026-10-30 — Real content is mounted, never committed (spec 003)
|
||||
|
||||
**Decision:** the real audio, timed lyrics, cover art and Homer texts stay in a folder outside the repo
|
||||
(`~/epic-the-musical-content` on the dev host), served under `/content/` from `CONTENT_DIR` and mounted
|
||||
read-only into the container. **Why:** the audio, lyrics and art are Jorge Rivera-Herrans' copyrighted
|
||||
work; committing them would push them to Gitea and bake 1.5 GB into the image. The user chose this
|
||||
("Mount it, never commit"). Range requests are handled in `src/content/routes.ts` so audio can seek.
|
||||
@@ -85,3 +85,6 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
|
||||
- Mockup 14: the cave-mouth mask may repeat as holes during the curtain drop (Chrome, oversized mask
|
||||
region). Fixed in 15 by shrinking the region; 14 left as is (superseded).
|
||||
|
||||
- Homer books: the last book (Odyssey XXIV, 250 paragraphs) also swallows the translator's notes and
|
||||
index after it. Trim at the notes heading before showing whole books.
|
||||
@@ -113,7 +113,8 @@ This file exists because of that request. It gets updated after every user messa
|
||||
2. Dispatch one agent per spike, at most 3 at a time. Point each at its report file and the brief, and tell it to continue from the checklist.
|
||||
3. Build 11.
|
||||
4. Rebuild the preview container from this branch:
|
||||
`docker build -t epic-timeline . && docker rm -f epic-timeline-preview && docker run -d --restart unless-stopped -e PORT=3000 -p 10.20.20.2:8099:3000 --name epic-timeline-preview epic-timeline`
|
||||
`docker build -t epic-timeline . && docker rm -f epic-timeline-preview && docker run -d --restart unless-stopped -e PORT=3000 -e CONTENT_DIR=/content -v /home/user/epic-the-musical-content:/content:ro -p 10.20.20.2:8099:3000 --name epic-timeline-preview epic-timeline`
|
||||
(the content mount was added 2026-10-30, spec 003)
|
||||
5. Ask the user which chart approach reads best.
|
||||
|
||||
## 2026-10-29
|
||||
@@ -304,6 +305,11 @@ What is in `~/epic-the-musical-content/` (1.5 GB, checked 2026-10-30):
|
||||
Docker image**; the server reads it from a `CONTENT_DIR` path, mounted read-only into the container.
|
||||
Needs the user's OK (it touches `src/server.ts`, the Dockerfile and the lyrics rule).
|
||||
|
||||
- **User's answers (2026-10-30):** content access: **"Mount it, never commit"**; next step: **"Yes, in 17
|
||||
after 16"** (16 finishes with its simulated player, then 17 uses real FLAC playback, line-synced
|
||||
`.lrc` lyrics and the actual Odyssey passage). Implemented as spec `docs/specs/003-content-mount.md`.
|
||||
- Note: Butler's translation uses Roman names (Ulysses, Minerva, Jove). Worth a line in the UI.
|
||||
|
||||
## Open questions (in priority order)
|
||||
|
||||
1. ~~Which sea-chart approach (08–11) reads best, or which mix?~~ 11's navigation + 10's style (§9).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
---
|
||||
status: in-progress
|
||||
---
|
||||
|
||||
# Plan 003 — content mount
|
||||
|
||||
Spec: `docs/specs/003-content-mount.md`. TDD: write the test, see it fail, implement, see it pass.
|
||||
|
||||
- [x] 1. `src/shared/lrc.ts` — `parseLrc(text): { time: number; text: string }[]`. Tests in
|
||||
`tests/content.test.ts`: basic lines, `[mm:ss]` and `[mm:ss.xx]`/`[mm:ss.xxx]`, multiple stamps on
|
||||
one line, `[ar:…]`-style metadata skipped, CRLF, sorted output.
|
||||
- [x] 2. `src/content/library.ts` — `scanLibrary(dir)`: saga folders `EPIC - <Name> (<year>)`, song files
|
||||
`… - NN - <Title>.flac`, sagas in story order (Troy, Cyclops, Ocean, Circe, Underworld, Thunder,
|
||||
Wisdom, Vengeance, Ithaca; unknown names after, by year then name), lyrics `.lrc` preferred else `.txt`, `folder.jpg` cover. URLs under
|
||||
`/content/files/` with each path segment URI-encoded. Fixture folder built in a temp dir by the test.
|
||||
- [x] 3. `src/content/homer.ts` — `splitBooks(text)`: between the Gutenberg START/END markers, headings
|
||||
`BOOK <roman>` (optional trailing dot), CRLF-safe; returns `{ book, heading, summary, paragraphs }[]`.
|
||||
Test with a small fixture string.
|
||||
- [x] 4. `src/content/routes.ts` — `contentResponse(req, dir | undefined): Response | null` handling the
|
||||
three routes, traversal guard, range requests (206 with Content-Range). Wire into `src/server.ts`
|
||||
`fetch()`. Route tests via `startServer` with `CONTENT_DIR` pointing at the fixture, and without it.
|
||||
- [x] 5. `.gitignore` / `.dockerignore`: ignore `epic-the-musical-content/`, `content/`, `*.flac`, `*.lrc`.
|
||||
- [x] 6. Docs: AGENTS.md (lyrics rule, content line in Architecture/Commands), `docs/ARCHITECTURE.md`,
|
||||
`docs/DECISIONS.md`, `docs/design/002-direction-round-notes.md` §7 run command.
|
||||
- [x] 7. Verify: `bun test`, `bun run typecheck`, `bun run build`; rebuild the preview container with the
|
||||
mount; curl library.json (9 sagas, 40 songs), a range request on a FLAC, an Odyssey book.
|
||||
@@ -0,0 +1,52 @@
|
||||
---
|
||||
status: approved
|
||||
---
|
||||
|
||||
# Real content from a mounted folder (audio, timed lyrics, Homer)
|
||||
|
||||
Status: approved by user 2026-10-30 · Date: 2026-10-30
|
||||
|
||||
## Intent
|
||||
|
||||
The user added the real content in `~/epic-the-musical-content/`: all 40 *EPIC* songs as FLAC, timed
|
||||
lyrics (`.lrc`) for 37 of them and plain-text lyrics for 3, saga cover art, and Samuel Butler's
|
||||
public-domain prose translations of the *Odyssey* and the *Iliad* (Project Gutenberg #1727, #2199).
|
||||
Mockups (starting with 17) and later the real app should use this content: real playback, line-synced
|
||||
lyrics, and the actual Odyssey passage for a song.
|
||||
|
||||
The audio, lyrics and cover art are Jorge Rivera-Herrans' copyrighted work. The user chose (2026-10-30):
|
||||
**"Mount it, never commit"** — the folder stays out of git and out of the Docker image; the server reads
|
||||
it from a path given by `CONTENT_DIR`, mounted read-only into the container.
|
||||
|
||||
## Goals
|
||||
|
||||
1. **Server serves the mounted folder** when `CONTENT_DIR` is set:
|
||||
- `GET /content/library.json` — every saga (name, year, cover URL) with its songs (track, title,
|
||||
audio URL, lyrics URL and whether they are timed), built by scanning the folder.
|
||||
- `GET /content/files/<path>` — the file at that path inside `CONTENT_DIR`, with the right content
|
||||
type and HTTP range support so audio can seek. Paths outside `CONTENT_DIR` are refused.
|
||||
- `GET /content/homer/<odyssey|iliad>/<book>.json` — one book of the Butler text: number, heading,
|
||||
summary line, paragraphs.
|
||||
- Without `CONTENT_DIR` (or with a missing folder), all `/content/*` routes answer 404 and every
|
||||
other route works as before.
|
||||
2. **Shared helpers** for clients: an LRC parser (timed lines, sorted, multi-stamp lines, metadata tags
|
||||
skipped) and the library types.
|
||||
3. **Nothing copyrighted is committed or baked into the image.** `.gitignore` and `.dockerignore`
|
||||
guard against a copy inside the repo.
|
||||
4. **Docs**: AGENTS.md lyrics rule updated (real lyrics only from the mounted folder, never in the
|
||||
repo); ARCHITECTURE, DECISIONS; the preview container run command mounts the folder.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Changing existing mockups 01–16 to use real content.
|
||||
- Transcoding FLAC (browsers play FLAC); caching or a CDN.
|
||||
- Mapping songs to Odyssey passages (that is content work for the real app).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- `bun test` covers: library scan of a fixture folder (sagas in story order, songs by track, `.lrc`
|
||||
preferred over `.txt`), LRC parsing, Homer book splitting, the `/content/*` routes with and without `CONTENT_DIR`,
|
||||
path-traversal refusal, and a range request returning 206.
|
||||
- The preview container, run with `-v ~/epic-the-musical-content:/content:ro -e CONTENT_DIR=/content`,
|
||||
serves `/content/library.json` listing 9 sagas and 40 songs, and an audio file plays with seeking.
|
||||
- `git status` shows no content files; the Docker image contains none.
|
||||
Reference in new issue
Block a user