Find the blind spots in your film education. Cinegaps compares your Letterboxd watch history against curated canonical lists and shows you exactly which great films you're missing, where the gaps fall by era, and which unwatched films would close the most ground across multiple lists at once.
- Letterboxd CSV import — drop your
watched.csvexport and get results instantly; no account connection or login required - 9 canonical lists across 3 categories — compare against Critics' Consensus (Sight & Sound, TSPDT, Ebert), Fan Favorites (Letterboxd Top 500, Most Fans, IMDb), and Award Winners (Oscars, Cannes, Venice); mix and match freely
- Blind spot profile — derives your viewing archetype (The Cinephile, The Anglophone, The New Waver, etc.) from your era and category coverage patterns, with stat bars showing the underlying signals
- Top picks — ranks unseen films by how many of your selected lists they appear on, surfacing the single highest-impact watches first
- Streaming availability — each top pick shows which flatrate services carry it in your region, so you can act on a recommendation immediately
- CSV export — download your top 10 blind spots as a Letterboxd-importable watchlist CSV
- Private by design — your watch history never leaves the browser; all processing is fully client-side with no server upload
| List | Category | Films | Scope | Bias |
|---|---|---|---|---|
| Sight & Sound Greatest Films 2022 | Critics' Consensus | 264 | All eras, all countries | Heavy toward European arthouse and silent/classic era; Chantal Akerman's Jeanne Dielman holds #1 |
| TSPDT 1,000 Greatest Films | Critics' Consensus | 1,000 | Broadest critical scope — all eras, all countries | Aggregate of hundreds of polls and guides; rewards consensus across multiple critical traditions |
| Roger Ebert's Great Movies | Critics' Consensus | 378 | Mixed — Hollywood classics through world cinema | Strong on American studio-era films, Italian neorealism, and personal favourites; excludes many recent releases |
| Letterboxd Top 500 | Fan Favorites | 500 | Weighted average rating across all Letterboxd users | Skews toward celebrated arthouse and prestige cinema; broadly overlaps with Sight & Sound |
| Letterboxd Most Fans | Fan Favorites | 100 | Films most often listed as a personal favourite | Skews toward genre cinema, contemporary releases, and cult favourites; very different from the ratings-based list |
| IMDb Top 250 | Fan Favorites | 250 | Highest-rated films on IMDb (min. votes threshold) | Dominated by English-language and mainstream cinema; over-represents blockbusters and recent releases |
| Oscar Best Picture Winners | Award Winners | 98 | One film per year, 1927/28–present | Rewards prestige Hollywood productions; historically poor track record with foreign-language and genre films |
| Cannes Palme d'Or Winners | Award Winners | 103 | One film per year, 1955–present; includes joint winners | Strong European arthouse slant; more adventurous than the Oscars; includes joint winners and honorary awards |
| Venice Golden Lion Winners | Award Winners | 83 | One film per year, 1945–present | The oldest major film festival award; broader international range than Cannes, with a history of rewarding challenging and formally daring work |
The profile section derives one of eleven archetypes from your coverage data:
| Archetype | Signal |
|---|---|
| The Newcomer | Overall coverage below 10% |
| The Completionist | Overall coverage above 65% |
| The Cinephile | Critics' lists lead by 12+ points over Fans and Awards |
| The Populist | Fan lists lead by 12+ points over Critics and Awards |
| The Ceremony Watcher | Awards lists lead by 12+ points over Critics and Fans |
| The Anglophone | IMDb coverage leads Sight & Sound + Cannes average by 20+ points |
| The Archivist | Classic era (pre-1960) coverage dominates by 15+ points |
| The New Waver | Golden era (1960–79) coverage dominates by 15+ points |
| The Nineties Kid | Modern era (1980–99) coverage dominates by 15+ points |
| The Modernist | Current era (2000–) coverage dominates by 15+ points |
| The Explorer | No dominant pattern |
cinegaps/
├── src/
│ ├── app/
│ │ ├── page.tsx Main page — all steps wired together
│ │ ├── layout.tsx Root layout and dark mode initialisation
│ │ └── api/
│ │ ├── poster/route.ts Proxy route: TMDB poster images
│ │ └── providers/route.ts Proxy route: TMDB streaming availability
│ ├── components/
│ │ ├── ArchetypeCard.tsx Blind spot profile card with era/category stat bars
│ │ ├── CsvUploader.tsx Drag-and-drop / click CSV upload area
│ │ ├── EraChart.tsx Era coverage bar chart
│ │ ├── ListSelector.tsx Category and list toggle UI
│ │ ├── RadialRing.tsx Circular coverage indicator per list
│ │ ├── ResultsPanel.tsx Seen/unseen film browser, grouped by list
│ │ ├── StepSidebar.tsx Sticky step-navigation sidebar
│ │ └── TopPicks.tsx Top picks grid with era/provider filters and CSV export
│ ├── lib/
│ │ ├── archetype.ts Derives viewer archetype from coverage data
│ │ ├── compare.ts Film matching, gap analysis, era stats
│ │ ├── export.ts Letterboxd-compatible CSV download
│ │ ├── letterboxd.ts URL and slug helpers for Letterboxd links
│ │ ├── parseCsv.ts Parses Letterboxd watched.csv exports
│ │ ├── poster.ts TMDB poster fetching with in-flight cache
│ │ ├── providers.ts TMDB streaming provider fetching with cache
│ │ ├── regions.ts TMDB region codes and browser locale detection
│ │ ├── types.ts Shared TypeScript types
│ │ └── __tests__/ Vitest unit tests for all core logic
│ └── data/
│ ├── index.ts Exports ALL_LISTS array
│ └── lists/ One .ts file per canonical list (9 total)
├── scripts/
│ └── populate-lists.ts Scrapes Letterboxd to refresh list data
├── vitest.config.ts Vitest configuration
└── public/ Static assets
| Layer | Technology |
|---|---|
| Framework | Next.js 16 (App Router) |
| UI | React 19 |
| Styling | Tailwind CSS 4 |
| Language | TypeScript 5 |
| Testing | Vitest |
| Posters & streaming | TMDB API |
Prerequisites: Node.js 18+
Movie posters and streaming availability are fetched from The Movie Database. Without an API key the app works normally — posters and provider info just won't load.
- Create a free account at themoviedb.org
- Go to Settings → API and copy your API key
- Create
.env.localin the project root:
TMDB_API_KEY=your_api_key_herenpm install
npm run devOpen http://localhost:3000 in your browser.
- Log in to letterboxd.com
- Go to Settings → Import & Export → Export Your Data
- Download and unzip — use the
watched.csvfile
- Drop your
watched.csvonto the upload area - Select which canonical lists to compare against (all selected by default)
- Review your coverage rings, blind spot profile, and top picks
- Use Export to download a Letterboxd-importable watchlist CSV
The core logic in src/lib/ is covered by unit tests using Vitest. No browser or React setup required — these are pure function tests.
# Run tests once
npm test
# Watch mode
npm run test:watchWhat's covered:
| File | What's tested |
|---|---|
compare.test.ts |
Title normalisation (articles, punctuation), ±1 year tolerance, compare/compareAll, crossListGaps deduplication and sort, buildEraStats era bucketing and cross-list deduplication |
archetype.test.ts |
All 11 archetypes, threshold boundary values, edge cases (empty results, missing categories, single active era) |
parseCsv.test.ts |
Valid rows, Windows CRLF line endings, quoted fields with embedded commas, missing/invalid year (skipped), invalid rating (→ undefined) |
letterboxd.test.ts |
Slug generation with colons, apostrophes, numbers, and multiple spaces |
regions.test.ts |
Locale extraction, language tag without region segment, unknown region code, navigator unavailable |
export.test.ts |
CSV header and rows, double-quote escaping, default and custom filenames |
Each list file in src/data/lists/ has a listUri pointing to its source Letterboxd list. The populate-lists script scrapes those pages and rewrites the films array in-place with live title, year, and director data.
# Refresh all lists
npm run populate-lists
# Refresh a single list
npm run populate-lists -- --list imdb-top250
# Preview changes without writing
npm run populate-lists -- --list sight-and-sound-2022 --dry-runThe script fetches each film's Letterboxd page to extract structured data (title, year, director) via JSON-LD. Requests are rate-limited automatically — a full refresh of all 9 lists takes several minutes.
Available list IDs: sight-and-sound-2022, tspdt-1000, roger-ebert, letterboxd-official-500, letterboxd-most-fans-100, imdb-top250, oscar-best-picture, cannes-palme-dor, venice-golden-lion.
npm run build
npm startContributions are welcome. A few ways to help:
- Add or refresh a canonical list — run
npm run populate-listsand open a PR with updated data - Improve film matching — the title normalisation in
src/lib/compare.tshandles most cases but edge cases exist; tests live insrc/lib/__tests__/compare.test.ts - Fix a bug or add a feature — open an issue first to discuss the approach, then submit a PR against
main
Please keep PRs focused — one change per PR makes review faster. Run npm test before submitting.
- Bug reports and feature requests — open an issue
- Questions and broader discussion — start a discussion
Feedback on the archetype logic, list selection, or matching accuracy is especially welcome.
MIT — see LICENSE for details.