No description
  • Python 67.8%
  • HTML 17.7%
  • Jinja 11.7%
  • JavaScript 1.9%
  • Dockerfile 0.9%
Find a file
matt 7afd826a27 Add in-card trailer embeds and refresh movie card layout
- Restructure cards: full-width title/meta on top, poster left + synopsis right
- Directory list is always two columns with ellipsis-truncated titles (year always shown); Screenings listed above New Releases
- Directory links scroll to a card without writing the URL hash, so a refresh starts at the top
- Resolve best YouTube trailer from TMDB (videos) with one-time cache self-heal for legacy entries
- Click-to-load trailer chip at the end of the synopsis injects an autoplaying embed full-width below the poster

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-31 14:30:55 -04:00
mockups Initial commit: ShowTimeFinder NYC showtime aggregator 2026-05-30 20:14:42 -04:00
src/showtimefinder Add in-card trailer embeds and refresh movie card layout 2026-05-31 14:30:55 -04:00
tests Initial commit: ShowTimeFinder NYC showtime aggregator 2026-05-30 20:14:42 -04:00
.dockerignore Initial commit: ShowTimeFinder NYC showtime aggregator 2026-05-30 20:14:42 -04:00
.env.example Add docker compose build with .env for TMDB key 2026-05-31 11:37:43 -04:00
.gitignore Add docker compose build with .env for TMDB key 2026-05-31 11:37:43 -04:00
compose.yaml Add docker compose build with .env for TMDB key 2026-05-31 11:37:43 -04:00
Dockerfile Use chromium-headless-shell instead of full Chromium 2026-05-31 12:46:49 -04:00
pyproject.toml Use chromium-headless-shell instead of full Chromium 2026-05-31 12:46:49 -04:00
README.md Add docker compose build with .env for TMDB key 2026-05-31 11:37:43 -04:00

ShowTimeFinder

Aggregated movie showtimes for New York City's independent and repertory theaters, on one fast page. Every night a pipeline scrapes each theater, collapses the same film playing at different venues into a single entry, enriches it with poster/synopsis/credits from TMDB, and renders a self-contained static site.

No database, no runtime backend — just a build step that emits HTML + JSON, served by any static web server (Caddy, in production).

Theaters covered

Theater Neighborhood Adapter
IFC Center Greenwich Village ifc
Film Forum West Village filmforum
Film at Lincoln Center Lincoln Square filmlinc
Angelika Film Center (Houston St) NoHo angelika
Village East by Angelika East Village angelika
Cinema 123 by Angelika Upper East Side angelika
Nitehawk (Williamsburg) Williamsburg nitehawk
Nitehawk (Prospect Park) Park Slope nitehawk
Cobble Hill Cinemas Cobble Hill cobblehill
BAM Rose Cinemas Fort Greene bam

Theaters that share a brand (the three Angelikas, the two Nitehawks) use one adapter and one logo; a per-venue location label disambiguates them in the UI.

How it works

adapters ──▶ screenings ──▶ resolver ──▶ films ──▶ render ──▶ public/
 (scrape)     (raw rows)     (TMDB +     (grouped   (Jinja2)    index.html
                              identity)   by film)               data/films.json
                                                                 assets/
  1. Scrape — one adapter per theater (src/showtimefinder/adapters/) fetches the source site and emits flat Screening rows. Each adapter is isolated: a broken site logs and is skipped, it can't sink the whole build.
  2. Resolveresolver.py maps each screening to a canonical film identity. With a TMDB_API_KEY set it pulls poster, director, runtime, genres and synopsis; without one it falls back to a title-normalized identity (no network). Results are cached in .tmdb-cache.json, which doubles as a hand-editable override store.
  3. Group — screenings for the same film across theaters collapse into one Film with all its Showings. Title normalization unifies things like "Part Two" / "Part 2" and strips theater framing ("… Presents:", "(35mm)", "70th Anniversary").
  4. Renderbuild.py renders index.html from templates/index.html.j2, writes the normalized data/films.json (a first-class artifact / read-API), and copies assets/. Output is staged then atomically moved into place, so a reader never sees a half-built site.

Quick start

Requires Python 3.9+.

python -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
playwright install chromium      # the Angelika adapter renders a JS SPA

# Optional: richer film metadata (poster, synopsis, credits)
export TMDB_API_KEY=your_tmdb_v3_key_or_v4_token

python -m showtimefinder.build --out public

Then open public/index.html, or serve the folder:

python -m http.server -d public 8000

Build command

python -m showtimefinder.build [--out DIR]
  • --out — output directory (default public). In production this is the folder Caddy serves.

TMDB_API_KEY (or TMDB_TOKEN) is read from the environment. A v4 read-access bearer token and a v3 API key are auto-detected. Omit it and the build still works, just with sparser metadata.

Deploy (Docker + Caddy)

The image wraps the exact same build command; the container and the bare script behave identically. It writes the site to /out, meant to be bind-mounted to the folder Caddy serves.

cp .env.example .env        # then put your TMDB_API_KEY in it
docker compose run --rm build

compose.yaml builds the image, loads .env, and writes the site into ./public. Point Caddy at that folder. Because the publish step moves files atomically, it's safe to rebuild while the site is being served.

Run it nightly via cron (Compose resolves paths relative to the repo, so cd in first):

0 4 * * * cd /home/docker/showtime-finder && /usr/bin/docker compose run --rm build >> build.log 2>&1

Prefer plain docker run (no Compose)? The equivalent is:

docker build -t showtimefinder .
docker run --rm --env-file .env -v "$PWD/public:/out" showtimefinder

Adding a theater

  1. Add an entry to src/showtimefinder/theaters.json with name, url, address, neighborhood, and a scraper key. Add a location field for venues that share a brand with another theater.
  2. If it's a new brand, write an adapter in src/showtimefinder/adapters/ (subclass Adapter, implement fetch() returning Screenings) and register it in adapters/__init__.py's SCRAPERS map.
  3. Add a test with a saved fixture under tests/fixtures/ so the adapter is pinned against a real page snapshot.

New theaters with no adapter yet can use "scraper": "fake" as a placeholder.

Theater logos

Each theater block is branded with a wide wordmark. Logos live in src/showtimefinder/assets/theaters/wordmarks/ and are wired up in the WORDMARKS dict in build.py, keyed by brand (the scraper value). Each entry is (filename, css_treatment), where the treatment normalizes any source art to read on the dark theme:

  • invert — dark/colored mark on transparent → forced white.
  • screen — light mark baked onto a dark box → the box drops out via mix-blend-mode: screen.
  • none — already light/colored on transparent → used as-is (e.g. Nitehawk's native-color reel).

A brand with no wide transparent wordmark simply omits its entry; the template then renders a clean text label instead of a broken square. To light one up, drop a wide transparent logo in wordmarks/ and add a line to WORDMARKS.

Data artifact

public/data/films.json is a first-class output, not just a render byproduct: a normalized, stable dump of every film and its showings (generated_at, then films[] with title, year, director, runtime, poster, genres, overview, and showings[]). It's a ready-made read-API for anything else that wants the data.

Tests

pytest

Adapter tests run against saved HTML/JSON fixtures (tests/fixtures/), so they're deterministic and don't hit the network. test_pipeline.py covers grouping and rendering end to end.

Project layout

src/showtimefinder/
  build.py            scrape → resolve → group → render entrypoint (python -m showtimefinder.build)
  resolver.py         film identity + TMDB enrichment with JSON cache
  models.py           Screening / Showing / Film dataclasses
  theaters.json       theater registry (name, url, address, scraper, location)
  adapters/           one scraper per theater brand
  templates/          index.html.j2
  assets/theaters/    brand wordmark logos
tests/                adapter + pipeline tests with fixtures
Dockerfile            wraps the build command for nightly container runs