Skip to content

rsvptr/celluloid

Repository files navigation

Celluloid logo

Celluloid

A private movie and TV tracker that turns your taste into what to watch next. You log what you have seen and how you felt about it, and Claude reads the whole picture to suggest your next film or show. It runs on your own database, behind your own login.

Live app: mycelluloid.vercel.app

Next.js React TypeScript Tailwind CSS

Prisma Neon Postgres Anthropic Claude TMDB Deployed on Vercel

Note

Celluloid is built for one person: you. The live app at mycelluloid.vercel.app is real, but it sits behind a login, so there is nothing to browse without an account. You bring your own Neon database, your own TMDB token, and your own Anthropic key, then deploy your own copy. Sign ups are closed by default, opened with a single environment variable only long enough to create your account.

Most trackers are good at storing what you watched and bad at the only question that matters afterward, which is what to watch tonight. Celluloid is built the other way around. Logging is quick, but everything feeds one feature: a recommendation engine that reads your ratings, your notes, the things you abandoned, what you watched recently, and the languages you actually lean toward, then asks Claude for titles that fit. You can run that inside the app, or copy the exact same brief as a prompt to paste into any other AI.

Contents

What it does

  • Tracks films and TV in one library, with TMDB metadata, posters, seasons, and episodes pulled in automatically
  • Every matched title shows where to stream, rent, or buy it (region-aware, via JustWatch data), a trailer link, and a "More like this" row you can add from in one click
  • Half star ratings from 0.5 to 10, private notes, favorites, and free form tags
  • Per episode and per season tracking for shows, with a progress bar, quick "mark season" and "mark show" actions, and a quiet "New" badge once a watching show has an aired episode you have not logged yet
  • Every viewing is logged to a private watch history, with an optional note. Logging again on a title you already finished counts as a rewatch instead of overwriting the first watch
  • Five watch states that map to a real backlog: watchlist, watching, watched, on hold, dropped
  • AI recommendations from Claude, based on a detailed picture of your taste, with mood presets, language, genre, and era preferences, and the option to base them on your whole library, your recent watches, or a hand picked set
  • A "copy as AI prompt" export, so the same brief works in any chat assistant
  • Search, rich filtering, two layouts, and bulk editing across a large library
  • Soft delete: removing a title moves it to Trash with every piece of personal data intact, ready to restore or delete forever
  • Uploading a spreadsheet stages a review: Celluloid proposes a TMDB match for every row, you fix or exclude what is wrong, then commit in resumable batches you can leave and pick back up later
  • A surprise picker that pulls something random off your watchlist when you cannot decide
  • Stats built from real activity: a heatmap and streaks, rating distribution, taste and totals by genre, and your most rewatched titles
  • Public, read only share links for a list or your whole library, with an optional expiry and a revoke switch that pulls access without deleting the link's record
  • A full JSON backup you can restore with a preview first, so nothing writes to your library until you confirm exactly what will change
  • Time zone and a default watch region in Settings, used for stats day boundaries and for which region's streaming and rental info you see
  • Exports to plain text, Markdown, JSON, and a styled Excel workbook, scoped as finely as language, genre, minimum rating, and release years, with filenames that say what is inside
  • Accounts with email and password, optional two factor, and a switch to close public sign ups

The recommendation engine

This is the reason the app exists. When you ask for suggestions, Celluloid does not send a flat list of titles. It builds a structured brief that separates the signals by how much they tell Claude about your taste, then it cleans up the results.

How the brief is built

flowchart TD
    A["Your library"] --> B{"Sort by signal strength"}
    B --> C["Watched and rated high<br/>(your strongest signal)"]
    B --> D["Rated low<br/>(an explicit avoid signal)"]
    B --> E["Watched, not yet rated"]
    B --> F["Recently watched<br/>(your current mood)"]
    B --> G["Currently watching"]
    B --> H["Abandoned and dropped<br/>(avoid things like these)"]
    B --> I["Watchlist<br/>(already planned, do not repeat)"]
    C --> J["Taste brief"]
    D --> J
    E --> J
    F --> J
    G --> J
    H --> J
    I --> J
    J --> K["Claude (structured JSON output)"]
    K --> L["Dedupe by TMDB id and by name plus year"]
    L --> M["Drop anything already in your library or watchlist"]
    M --> N["Re match to TMDB for posters and a one click add"]
    N --> O["Sort by confidence, then trim to the count you asked for"]
Loading

A few details that make the output better:

  • High and low both count. Titles you rated highly drive the positive direction. Titles you rated low get their own block that tells Claude to avoid similar things, so half of your signal is not wasted.
  • Language lean is computed, not assumed. The brief states the languages you actually rate and favorite, so a model does not default to English language picks when your taste runs elsewhere.
  • Recency reflects mood. Titles you finished in the app recently are weighted as your current direction. Imported titles that never had a real watch date stay quiet, so the app never fakes recency.
  • No repeats. Suggestions are checked against your whole library by TMDB id and by a normalized name plus year, which also catches regional titles that were never matched to TMDB.
  • Honest confidence. Each pick comes back with a confidence level, and the list is sorted high to low before it is trimmed, so a strong pick is never dropped in favor of a weak one.

Where suggestions come from

Basis What it uses Good for
Whole library Everything you have logged General "what next"
Recent watches Your most recent finishes (last 10, 20, or 50) Matching your current mood
Pick titles A set you choose by hand "More things like these three"

You can steer a run with a free text focus ("cozy mysteries", "something like Bramayugam", "90s sci fi") or tap a mood preset. Three dropdowns narrow the pool further: original language, genre, and era, from the 2020s back to before 1970. A preference becomes a hard requirement in the brief, confirmed matches float to the top of the results, and your dial settings persist for the browsing session. A "show different" button keeps the same brief but excludes everything already shown, so you can keep pulling fresh ideas. Unmatched suggestions still appear with a "find on TMDB" link so you can add them by hand.

Results stream in live. The app doesn't wait for the whole batch: Claude's response is parsed as it streams, each suggestion is validated, deduplicated, and matched to TMDB the moment it completes, and cards appear one by one with a status line ("thinking", "curating picks", a running count) and a Stop button. Once enough suggestions have been accepted, generation is aborted server-side, so you never pay for output past what you asked for.

Models. Claude Opus is the default. Sonnet and Haiku are selectable per run from the recommend page. The app adapts the request to each model: Opus 4.8 and Sonnet 5 use adaptive thinking and an effort setting, Haiku skips the options it does not support. Output is constrained to a JSON schema, and the taste brief carries an Anthropic prompt-cache breakpoint, so once your library is large enough to clear the cache minimum, a "Show different" re-run a few minutes later reprocesses only the short request block instead of your whole taste brief.

Bring your own key. Add an Anthropic key in settings and it is encrypted at rest with AES 256 GCM before it touches the database. A deployment wide key can also be set as a fallback.

Tracking your watch history

Every title carries personal tracking on top of its TMDB metadata.

Status Meaning
Watchlist Want to watch
Watching In progress
Watched Finished
On hold Paused for now
Dropped Gave up on it
  • Half star ratings run from 0.5 to 10. Click the left or right half of a star, or use the keyboard (arrow keys nudge by half or whole steps, 0 clears).
  • Notes are private and double as context for the AI. Favorites and tags layer on top, and tags also work as a recommendation lens.
  • TV is tracked properly. Shows expand into seasons and episodes. Tick a single episode, a whole season, or the whole show. Progress is denormalized for a fast bar on the card, and finishing a show stamps a watch date so it counts toward recency and stats. Once you are watching a show, an aired episode you have not logged shows a small "New" badge on its card.
  • Log watch records a dated viewing with an optional note, from the title page. The first log on a title stamps its watch date; logging again on an already watched title is recorded as a rewatch and adds to the running count on that title and on the stats page, without disturbing the original watch date.

The library

The home screen is your whole collection, in a poster grid or a dense list.

Control Options
Search Live filter by title
Type Movies, TV, or both
Status Any of the five watch states
Language Any original language in your library
Genre Any genre present
Rating Unrated, or 5 and up through 9 and up
Tag Any tag you have created
Needs match Only titles with no TMDB match, for cleanup
Sort Recently added, recently watched, name, release date, your rating, TMDB rating

On phones the filters collapse behind a single toggle and lay out as a clean two column drawer. Your filter, sort, and layout choices live in the URL, so a filtered view is bookmarkable and shareable, and pressing Back after opening a title returns you to exactly the view you left while you work through a backlog. In the dense list view, offscreen rows skip rendering entirely (content-visibility), so even a very long list stays fast.

Bulk editing. Switch on select mode and act on many titles at once: set a status, add or remove a tag, favorite or unfavorite, share, or remove. Selection is always scoped to what is visible, so a bulk action can never touch a hidden title.

Surprise me. One press picks a random title from the current view, preferring whatever is still on your watchlist, and takes you straight to it. Filter down to unwatched Malayalam horror first and the dice roll respects it.

Export these. When filters are active, a one click link opens the export page with the same scope already applied, so "everything tagged for horror night as a spreadsheet" is two clicks.

Command palette. Press the search button or use the keyboard shortcut to jump to any title or page from anywhere.

Stats

The stats page reads your activity rather than just counting rows.

  • Totals for titles, movies, shows, movies watched, episodes watched, and rewatches
  • An estimated watch time, with an honest note that episodes without a known runtime are estimated at about 42 minutes each
  • A watch activity heatmap and current and longest streaks, built from real in app watch dates
  • Titles by release year as a sparkline, a rating distribution histogram, and a by decade breakdown
  • Your top rated titles, your most rewatched titles, and a by status and by language breakdown
  • Taste by genre: your average rating per genre, for genres with at least two rated titles, alongside a top genres count by how often each appears in your library

Because the imported backlog starts without watch dates, the activity views begin empty and fill in as you mark things watched in the app. The page says so plainly rather than showing a misleading blank.

Exports

Everything in your library can leave in the shape you need. Pick a scope (type, status, language, genre, minimum rating, release year range, favorites only, a tag) and a format, then copy or download.

Format File Best for
AI prompt .txt Pasting into any chat assistant for recommendations
Text .txt A quick human readable list
Markdown .md A table you can drop into notes or a gist
JSON .json Feeding another tool
Excel .xlsx A styled workbook with separate Movies and TV sheets

The AI prompt export uses the exact same taste brief as the in app engine. Even when you scope the export down, the "do not recommend these" guardrails are still drawn from your full watchlist and dropped lists, so a scoped prompt never contradicts itself.

Every download gets a unique, descriptive filename, for example celluloid-library-movie-malayalam-8plus-1990-1999-20260613-101500.xlsx, so repeated exports never overwrite each other and a saved file tells you what it holds at a glance.

Backups and recovery

Celluloid has an application-level backup for the personal data that would be painful to rebuild. In Settings > Backup, choose Download backup to save a versioned JSON file. The current v2 format contains movie and TV metadata, soft-deleted titles, status, ratings, favorites, notes, watch dates and watch-event history, full season and episode progress, tags and their title joins, owner timezone/region preferences, and ordered shared-list settings including expiry and revocation. Restore also accepts v1 files through an explicit compatibility upgrade, for files up to 4 MB. Backups deliberately exclude passwords, sessions, two-factor secrets and backup codes, encrypted API keys, and share slugs. A restored shared list receives a new unguessable link instead of reviving an old bearer token.

To restore, select the JSON file and a mode, then choose Preview restore. The preview reports how many titles will be created, updated, left unchanged, or skipped as conflicts. Nothing is written until you approve the confirmation dialog.

  • Merge without replacing personal data adds missing titles and nested records, fills empty nullable values such as a rating, note, or watch date, and unions tags. It keeps existing status, favorites, counters, and episode progress.
  • Use backup personal data makes the backup authoritative for status, ratings, notes, watch dates, favorites, tags, and episode progress on matching titles. Titles absent from the backup remain in the library.
  • Titles match first by media type and TMDB id, then by normalized name and release year when an id is unavailable. Ambiguous matches are conflicts and are skipped. Repeating the same restore is safe: already-restored records are recognized rather than duplicated.

Treat the JSON as private library data. Keep at least one copy off the deployment, preferably in encrypted storage, and download a fresh copy before migrations or large imports. This file complements Neon recovery; it does not replace a database restore point because it intentionally omits authentication state and secrets.

Neon operator actions

These steps are manual owner responsibilities, not actions the application performs:

  1. Check point-in-time recovery now. Open the production project in the Neon Console, go to Settings > Instant restore, and confirm that the restore window is non-zero. Record the selected duration somewhere outside the database. Neon's restore-window guide explains current plan limits, storage cost, and the fact that this setting applies to every branch in the project.
  2. Set retention to the time you need to notice a mistake. For this private workflow, choose the longest window you can justify rather than assuming the plan default is enough. Recheck it after a plan change, project move, or billing change. If the plan supports scheduled snapshots, configure and review their retention under Backup & restore; deleted snapshots cannot be recovered.
  3. Create a recovery point before risky work. Download the Celluloid JSON backup. On a root production branch, also create a manual Neon snapshot before a schema migration, bulk import, or repair. Follow Neon's current Backup & restore guide, since snapshot availability and limits are plan-dependent.
  4. Run a restore drill at least quarterly. Use Neon's preview-data tools or a multi-step snapshot restore to create an isolated branch. Do not point the production deployment at it. Connect with the scratch branch's own connection string and check representative counts and records, including titles, tags, watched episodes, notes, and ratings. Preview the Celluloid JSON restore against that scratch copy as a second check. Record the date and result, then delete the scratch branch after verification.
  5. For a real incident, inspect before replacing production. Stop avoidable writes, download the current application backup if possible, choose a timestamp or snapshot from before the damage, and use Neon's read-only preview to confirm the data and schema. Restore only after that check. Neon keeps the pre-restore branch as a backup branch; verify Celluloid sign-in, title counts, TV progress, notes, tags, and shares before deleting it.

Sharing

You can publish a read only view of your library, or an ordered selection, at an unguessable link under /s/. Shared pages need no login and are marked no index. Notes are hidden unless you opt in, and a whole library share hides your not yet watched watchlist by default so you are not broadcasting your plans. Links can be permanent or expire after 7, 30, or 90 days. Every link remains listed in settings, where you can copy it, revoke access without losing its record, or delete it permanently.

Privacy and security

This is your data on your infrastructure.

  • Self hosted on your own Neon database. There is no shared backend and no third party account system.
  • Every query is scoped to the signed in user, so one account can never read another's titles, tags, shares, or settings.
  • Your Anthropic key is encrypted at rest with AES 256 GCM. It is never stored or logged in plaintext.
  • Accounts use email and password through Better Auth, with optional time based two factor (an authenticator app, with backup codes and a manual setup key). Sessions are stored in the database, and deleting your account requires your password, not just a live session.
  • Sign in, sign up, password change, and two factor verification are rate limited against brute force, and the AI, search, import, and export routes are rate limited per user to keep a runaway loop from draining your API budget.
  • Public sign ups are closed by default. Set ALLOW_SIGNUPS=true only long enough to create your own account, then remove it, which is the recommended locked down state for a personal deployment.
  • Security headers are set for every response: frame denial, no sniff, a strict referrer policy, a content security policy covering framing, plugins, base tags, and form targets, a permissions policy that turns off browser capabilities the app never uses, and HSTS on the production domain.
  • Server side input validation bounds every free text field, and npm audit runs clean with pinned overrides for transitive advisories.

Architecture

Celluloid is a normal Next.js App Router application. Pages render on the server, talk to Postgres through a Prisma driver adapter, and reach two outside services: TMDB for metadata and Anthropic for recommendations. There is no separate API server to run.

flowchart LR
    subgraph Browser
      UI["Library, detail, recommend,<br/>stats, export, settings"]
    end
    subgraph "Next.js 16 (App Router)"
      direction TB
      RSC["Server components<br/>and server actions"]
      API["Route handlers<br/>(recommend, search, import, backup, xlsx)"]
      AUTH["Better Auth<br/>(sessions, 2FA)"]
    end
    DB[("Neon Postgres<br/>via Prisma 7")]
    TMDB["TMDB API<br/>(metadata, posters)"]
    CLAUDE["Anthropic Claude<br/>(suggestions)"]

    UI --> RSC
    UI --> API
    RSC --> DB
    API --> DB
    RSC --> AUTH
    API --> AUTH
    AUTH --> DB
    API --> TMDB
    RSC --> TMDB
    API --> CLAUDE
Loading

Reads and simple mutations go through server components and server actions. The work that can be slow or large (asking Claude, searching TMDB, parsing an uploaded sheet, building an Excel file) runs in route handlers with a longer timeout. The Anthropic call is streamed and the recommendation response is constrained to a JSON schema, so the server gets clean structured data back instead of free text to parse.

Data model

A user owns titles, tags, and share lists. A title can be a movie or a show. Shows own seasons, seasons own episodes. Every logged viewing appends a WatchEvent, an append-only record separate from the title's own status/rating/watchedAt fields. Tags attach to titles through a join table. Auth tables (sessions, accounts, two factor) hang off the user as well.

erDiagram
    USER ||--o{ TITLE : owns
    USER ||--o{ TAG : owns
    USER ||--o{ SHARELIST : owns
    USER ||--o{ SESSION : has
    USER ||--o{ TWOFACTOR : has
    TITLE ||--o{ SEASON : has
    SEASON ||--o{ EPISODE : has
    TITLE }o--o{ TAG : "tagged via TitleTag"
    TITLE ||--o{ WATCHEVENT : logs

    TITLE {
      string id
      enum   mediaType
      string name
      enum   status
      float  rating
      string notes
      bool   favorite
      date   watchedAt
      int    watchedEpisodes
    }
    SEASON {
      int seasonNumber
      int episodeCount
    }
    EPISODE {
      int  episodeNumber
      bool watched
    }
    WATCHEVENT {
      enum kind
      date occurredAt
      string note
    }
Loading

Titles are unique per user by media type and TMDB id, and the denormalized watchedEpisodes count keeps the progress bar fast without walking every episode on each render. A staged spreadsheet upload gets its own short-lived ImportJob and per-row ImportItem rows (see Bringing in your library); they hold no personal library data and are not part of the core model above.

Tech stack

Area Choice
Framework Next.js 16, App Router, Turbopack
UI React 19
Language TypeScript
Styling Tailwind CSS 4
Database Postgres on Neon
ORM Prisma 7 with the pg driver adapter
Auth Better Auth, with the two factor plugin
Metadata TMDB (The Movie Database)
AI Anthropic Claude (Opus, Sonnet, Haiku)
Animation Motion
Command palette cmdk
Dialogs Radix UI
Toasts Sonner
Icons Lucide
Spreadsheets ExcelJS
Tests Node test runner through tsx

Getting started

You need Node.js 22 or newer, a Neon Postgres database, a TMDB API Read Access Token, and (for AI features) an Anthropic API key.

1. Install dependencies. This also generates the Prisma client through a postinstall step.

npm install

2. Set up your environment. Copy the example file and fill in real values. See Environment variables for what each one is.

cp .env.example .env

3. Create the database schema. This applies the migrations to your Neon database.

npm run db:deploy

4. Start the dev server.

npm run dev

Open http://localhost:3000, create your account, then add a few titles by searching TMDB. Add an Anthropic key in settings and open the recommend page to see suggestions. Once your account exists, remove ALLOW_SIGNUPS to close the door behind you.

Environment variables

Copy .env.example to .env. Never commit .env; it is already ignored.

Variable Required What it is
DATABASE_URL Yes Neon pooled connection string (host contains -pooler). Used at runtime.
DIRECT_URL For migrations Neon direct connection string (no pooler). Used only for migrations. Falls back to DATABASE_URL if unset.
TMDB_ACCESS_TOKEN Yes The TMDB v4 API Read Access Token (the long token starting with eyJ). Server side only.
BETTER_AUTH_SECRET Yes A long random secret for signing sessions.
BETTER_AUTH_URL Yes The app base URL. Local is http://localhost:3000, production is your deployed URL.
NEXT_PUBLIC_SITE_URL Yes Public base URL for metadata and the auth client. Match BETTER_AUTH_URL.
ENCRYPTION_KEY Required in production (dev/test may fall back to BETTER_AUTH_SECRET) Key for encrypting per user Anthropic keys at rest.
ANTHROPIC_API_KEY Optional A deployment wide Claude key, used when a user has not added their own.
ALLOW_SIGNUPS Optional Public sign ups are closed unless this is true. Set it to bootstrap your account, then remove it. Existing users can always sign in.

The old DISABLE_SIGNUPS flag is deprecated. For one release, DISABLE_SIGNUPS=false is still honored as ALLOW_SIGNUPS=true and the server logs a one time warning. Move to ALLOW_SIGNUPS and drop the old variable.

Bringing in your library

There are three ways to get titles in, and they all enrich from TMDB (posters, seasons, episodes, genres, runtime, original language).

  1. Search and add. The fastest path for a handful of titles. Search TMDB inside the app and add with one click.
  2. Upload a sheet. The in app importer on the Add page accepts an .xlsx or .csv file with a Title column, up to 2 MB and 250 rows.
  3. Import the legacy workbook. If you are coming from a "Movies and TV Shows Watched" style spreadsheet with separate sheets, drop it at data/watched.xlsx and run the importer.

Uploading a sheet stages a review before anything touches your library. Celluloid proposes a TMDB match for each row and shows a confidence label. From there you can:

  • Exclude a row you do not want, or retry one that failed
  • Search TMDB yourself and pick the exact match if the proposal is wrong or missing
  • Commit once you are happy, which runs in small resumable batches rather than one long request

Nothing is written until you commit, and committing itself is safe to interrupt: leave the page mid-batch and the same job is offered for resume the next time you open Add, picking up right where it left off. Cancelling abandons whatever has not committed yet, but keeps anything already saved. A row that keeps failing can be retried up to a fixed number of attempts before it needs a manual match.

Imported titles never get an invented watch date or a fabricated history entry, since the sheet does not carry one. A row marked watched creates the title with its status set but no WatchEvent, so the recommendation engine's recency signal and the stats activity views stay quiet for it until you actually watch or log it in the app.

npm run import

The legacy workbook importer bypasses the review step entirely (it is meant for a one-time bootstrap from an existing spreadsheet, not repeat uploads). It reads the title, release date, and status columns, matches each row to TMDB (with a guard that prevents a wrong poster from being attached to a transliterated or regional title), and pulls season and episode structure for shows. It is idempotent: re-running it updates metadata but never overwrites a rating, note, or watch status you already set. npm run db:seed runs the same import through Prisma's seed hook; use whichever of the two commands fits your workflow. Your own data/watched.xlsx is kept out of git, since it is personal.

Scripts

Script What it does
npm run dev Start the local dev server
npm run build Production build
npm run start Run the production build locally
npm run lint Run ESLint
npm test Run the test suite on Node's built in runner (Node 22 or newer)
npm run db:deploy Apply migrations to the database
npm run db:migrate Create and apply a new migration in development
npm run db:generate Regenerate the Prisma client
npm run db:push Push the schema straight to the database, no migration file. For prototyping only; use db:migrate for a real change
npm run db:studio Open Prisma Studio to browse the data
npm run db:seed Prisma's seed hook, wired to the same legacy workbook import as npm run import
npm run import Import the legacy Excel workbook from data/watched.xlsx

Deploying to Vercel

The app is a standard Next.js project and runs well on Vercel.

  1. Push this repository to GitHub.
  2. Import the project into Vercel.
  3. Add every variable from the Environment variables table in the Vercel project settings. Set BETTER_AUTH_URL and NEXT_PUBLIC_SITE_URL to your real deployed URL.
  4. Deploy. The build runs prisma generate, applies migrations with prisma migrate deploy, then builds Next.

A short checklist for a clean first deploy:

  • Use fresh secrets in production. Rotate anything that has been on a local machine: the Neon password, the TMDB token, the Anthropic key, BETTER_AUTH_SECRET, and ENCRYPTION_KEY.
  • Both BETTER_AUTH_URL and NEXT_PUBLIC_SITE_URL point at the production URL.
  • DATABASE_URL is the pooled string and DIRECT_URL is the direct one.
  • Set ALLOW_SIGNUPS=true for the first deploy, create your account on the live site, then remove it and redeploy.

Project structure

celluloid/
├── prisma/
│   ├── schema.prisma          data model
│   └── migrations/            Neon migration history
├── public/
│   ├── logo.png
│   └── icon-192.png
├── scripts/
│   ├── import-excel.ts        importer for the legacy workbook
│   └── db-check.mjs, db-dump.mjs   ad hoc, read only maintenance scripts (run manually before a migration)
├── src/
│   ├── app/
│   │   ├── (app)/             signed in pages: library, add, title, recommend, export, stats, settings
│   │   ├── api/               route handlers: recommend, search, titles, import (upload + staged jobs), backup, export/xlsx, auth
│   │   ├── login/
│   │   ├── s/[slug]/          public read only shared lists
│   │   ├── layout.tsx, globals.css, manifest.ts, robots.ts
│   │   └── error.tsx, not-found.tsx, global-error.tsx
│   ├── components/            library, cards, charts, dialogs, import review, command palette, rating stars, nav
│   ├── generated/prisma/      generated Prisma client (not committed)
│   ├── lib/                   auth, prisma, tmdb, recommend, export, import (legacy + staged review), backup, share, region and settings actions, data, actions, crypto, rate limiting
│   └── proxy.ts               Next 16 request proxy (this version uses proxy, not middleware; also sets the nonce-based CSP)
├── tests/                     pure logic tests: matching, export scope, filenames, prompt, crypto, backup, staged import, rate limiting
├── next.config.ts             security headers and the TMDB image allowlist
├── prisma.config.ts
├── .env.example
└── package.json

Keyboard and accessibility

  • A command palette opens from the header (⌘K on Mac, Ctrl+K elsewhere; the hint matches your platform) for fast navigation to any title or page.
  • The star rating is fully operable from the keyboard: arrow keys nudge by half or whole steps, Home and End jump to the ends, and 0 clears.
  • Every interactive control has a visible focus ring, icon only buttons carry labels, and toggles report their pressed state to screen readers.
  • The card hover lift and other motion respect the system "reduce motion" setting.

Notes and limitations

  • Celluloid is a personal tool, not a multi tenant service. It supports more than one account, but it is meant to be locked to one by leaving ALLOW_SIGNUPS unset after setup.
  • An imported backlog has no ratings or watch dates at first, so the recommendation quality and the activity stats both improve as you rate titles and mark things watched. The "unrated" filter is the quick way to work through that.
  • TMDB matching is automatic and usually right, but a transliterated or regional title can occasionally match the wrong entry. The "needs match" filter and the per title "change match" control are there to fix those by hand.
  • The in memory rate limiter bounds bursts per server instance. For a single user deployment that is plenty; a busy multi user instance would want a shared store.
  • Starting a new spreadsheet upload while an earlier one was left mid-review (never committed or cancelled) leaves that earlier job orphaned. Only the most recent active job is offered for resume, and there is no scheduled cleanup for the ones left behind yet.

About

A personal movie and TV tracker for everything you have watched. Rate titles, track shows episode by episode, organize with tags and favorites, and see your viewing stats. Export your library anywhere, or get recommendations tuned to your taste. Private, running on your own database.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages