On this page
Python is the shortest path from a question to an answer: what is on tonight in my city, which films get the most screens this week, where can I see this film in original version near me. This tutorial answers the three with requests and pandas, in four small files you can run as they are.
$ python tonight.py Lyon
Tonight in Lyon: 163 showtimes, 48 filmsWhat you need
- Python 3.9 or later.
- An API Cinéma key: create a free account, confirm your e-mail address, then create a key from the dashboard. The free plan gives 150 requests a week for trials and non-commercial projects; each script below costs one to three.
python -m venv .venv && source .venv/bin/activate
pip install requests pandas
export API_CINEMA_KEY="ac_live_…"A tiny client: api_cinema.py
Two helpers: get makes one call, all_pages walks a long list of showtimes page after page.
import os
import time
import requests
API = "https://api.api-cinema.com/v1"
session = requests.Session()
session.headers["Authorization"] = f"Bearer {os.environ['API_CINEMA_KEY']}"
def get(path, **params):
"""One GET on the API. Waits and retries once when the per-minute limit is reached (HTTP 429)."""
for attempt in range(2):
res = session.get(f"{API}{path}", params=params, timeout=30)
wait = int(res.headers.get("Retry-After", "0"))
# per-minute limit; past the weekly quota the wait runs to Monday: give up
if res.status_code == 429 and attempt == 0 and wait <= 60:
time.sleep(wait)
continue
res.raise_for_status()
return res.json()
def all_pages(path, **params):
"""Every row of a showtimes list, page after page, following meta.nextCursor."""
params.setdefault("limit", 1000)
while True:
page = get(path, **params)
yield from page["data"]
cursor = page["meta"].get("nextCursor")
if not cursor:
return
params["cursor"] = cursor- The key goes in the
Authorizationheader, read from the environment: never commit it. - Past the per-minute limit (60 requests per key on the standard plans) the API answers
429withRetry-After, the seconds to wait. Past the weekly quota it also answers429, but the wait runs to Monday: the script stops instead. meta.nextCursoris the stable way through a long result: 1,000 rows a page, no row skipped or repeated even when the programme changes in between.
What's on tonight: tonight.py
import sys
from collections import defaultdict
from datetime import date
from api_cinema import get
city = sys.argv[1] if len(sys.argv) > 1 else "Lyon"
today = date.today().isoformat()
# from / to are read in each cinema's local time: 18:00 means 6 pm in Lyon as in Chicago
res = get("/showtimes", city=city, **{"from": f"{today}T18:00", "to": f"{today}T23:59"}, limit=1000)
films = defaultdict(list)
for s in res["data"]:
films[s["movie"]["title"]].append(f"{s['startsAt'][11:16]} {s['cinema']['name']} ({s['version']})")
print(f"Tonight in {city}: {len(res['data'])} showtimes, {len(films)} films\n")
for title, times in sorted(films.items(), key=lambda f: -len(f[1]))[:10]:
print(title)
for t in sorted(times)[:5]:
print(" ", t)python tonight.py Lyon
python tonight.py Chicagofrom and to accept a date or a date and time, always in each cinema's own time zone: the same script works for a French and an American city. city is an exact match on the town name, so Paris covers its arrondissements but not its suburbs.
A week of showtimes in a spreadsheet: week_csv.py
all_pages fetches every showtime of the city over 7 days (a big city fits in two or three requests), pandas flattens them into a CSV and answers two questions.
import sys
from datetime import date, timedelta
import pandas as pd
from api_cinema import all_pages
city = sys.argv[1] if len(sys.argv) > 1 else "Lyon"
start = date.today()
end = start + timedelta(days=6)
rows = [
{
"date": s["startsAt"][:10],
"time": s["startsAt"][11:16],
"cinema": s["cinema"]["name"],
"film": s["movie"]["title"],
"tmdb_id": s["movie"]["tmdbId"],
"version": s["version"],
"format": s["format"],
"booking_url": s["bookingUrl"],
}
for s in all_pages("/showtimes", city=city, **{"from": start.isoformat(), "to": end.isoformat()})
]
df = pd.DataFrame(rows).astype({"tmdb_id": "Int64"}) # ids stay integers despite missing ones
df.to_csv(f"showtimes-{city.lower()}-{start}.csv", index=False)
print(f"{len(df)} showtimes saved, {start} → {end}\n")
print("Most programmed films this week:")
print(df.groupby("film").size().sort_values(ascending=False).head(10).to_string())
print("\nShare of original-version screenings (VO + VOST):")
print(f"{df['version'].isin(['VO', 'VOST']).mean():.0%}")$ python week_csv.py Lyon
1755 showtimes saved, 2026-10-31 → 2026-11-06
Most programmed films this week:
film
Moulin 129
Quelques mots d'amour 108
…The CSV opens in Excel, Google Sheets or a notebook. In France most cinemas publish the next week on Monday or Tuesday, for the Wednesday releases: run the script on Wednesday for a full week.
One film near me: near_me.py
Find the film by title, then ask for its showtimes around a point.
import sys
from api_cinema import get
title = sys.argv[1] if len(sys.argv) > 1 else "Karma"
lat, lng = 45.7640, 4.8357 # Lyon city centre: use your own coordinates
movies = get("/movies", q=title, playing="true", limit=5)["data"]
if not movies:
sys.exit(f"No film showing matches {title!r}")
movie = movies[0]
# tmdb_id gathers every showtime of the film, whatever the title each cinema publishes
film = {"tmdb_id": movie["tmdbId"]} if movie["tmdbId"] else {"movie_id": movie["id"]}
res = get("/showtimes", **film, near=f"{lat},{lng}", radius_km=5, limit=50)
print(f"{movie['title']}: next showtimes within 5 km\n")
for s in res["data"][:15]:
print(f"{s['startsAt'][:10]} {s['startsAt'][11:16]} {s['version']:<5} {s['cinema']['name']}")python near_me.py KarmaTwo cinemas may publish the same film under slightly different titles; tmdb_id (The Movie Database id) gathers them all. near=lat,lng with radius_km gives the showtimes sorted by time around any point, in a metro area that city would cut short.
Going further
- Every week, automatically.
crontab -e, then0 9 * * 3 cd ~/showtimes && .venv/bin/python week_csv.py Lyonexports the new week every Wednesday at 9 am. - Original versions. Add
version="VOST"(orVO) to any showtimes call for a film-club list. - Only what changed. For a database kept in sync,
updated_since=<ISO date>returns the showtimes added, changed or removed since your last run (removed ones haveremovedAtset). - Posters and booking. Every film has
posterUrl, every showtimebookingUrl.
Every parameter and field is in the documentation; the guide for France and the guide for the United States show the other searches.