The Theater
Implementation: src/server/services/theater.ts. UI: /theater/**.
Rows
homeRows({ userId, kidsMode }) returns, in order: Continue Watching (first — it is what people came back for), Trending, New Releases, Top Rated, Editor's Picks, Creator Spotlight, Coming Soon, and "Because you watched…".
Kids mode filters to G/PG. Children's is a curated-only genre creators cannot self-select — enforced in the publish form and in CURATED_ONLY_GENRES.
Recommendations
similarTo is content-based over signals we own, with transparent weights:
| Signal | Weight |
|---|---|
| Same universe (series sibling or shared parent project) | 60 |
| Shared style pack | 22 each |
| Same creator | 18 |
| Genre overlap | 12 each |
| Popularity prior | ≤ 8 |
This is genuine content-based filtering, and "Same universe" / "Similar visual style" are real rows rather than relabelled popularity. It is not the pgvector semantic engine Part 8 targets — that is a marked swap point: replace the scoring loop with a nearest-neighbour query, keep the signature.
Search
searchCatalog parses structured intent out of natural language, then keyword-matches the remainder.
Extracted: runtime (20-minute, under 10 minutes, feature-length), genres (canonical list + aliases like scifi, scary, cartoon), and maturity (family, adult).
The example from the spec — "20-minute dark sci-fi thriller with a female lead and a twist ending" — resolves to runtime ≈14–26 min, genres Sci-Fi + Thriller, keywords dark, female, lead, twist.
The UI shows how the query was interpreted. That is the difference between a search box that feels smart and one that feels broken, and it costs nothing.
Method is reported as structured_keyword, not dressed up as semantic. Filters beat embeddings for runtime and rating anyway, so the structured extraction stays useful after a vector swap.
Title pages
Synopsis, trailer slot, AI cast cards linked back to the Universe, creator, runtime, rating, reviews, comments, licensing terms shown as first-class facts, and "Same universe" / "Similar visual style" rows.
Player
components/theater/Player.tsx handles three cases and is explicit about which one you are in:
- Real video master → HTML5
<video>, resumes from watch history. - Procedural render (mock adapter) → animated preview on a simulated transport, with a note saying it is a style-accurate preview, not generated footage.
- No render → says so plainly. Progress still records, so Continue Watching works end to end.
Case 3 is the one worth getting right: a dead player with no explanation reads as a broken product.
Engagement
recordProgress (completion at 90%), incrementView, rate (recomputes the aggregate), comments with threading, follows, and My List as a toggle.
Film Festival
Monthly editions, category awards, audience voting. One vote per account per category, not per entry — otherwise a voter backs every nominee and the ranking means nothing. Voting for the same entry twice withdraws the vote.
Not built
- Adaptive HLS/DASH — needs a transcoding pipeline and Mux/Cloudflare Stream. Today the player serves the asset directly.
- Watch parties, remix marketplace transactions, monetization payouts.
- Creator-editable caption tracks. Captions generate from dialogue (
buildCaptions→ SRT) but there is no editor.