Aggregate movie showtimes across Colombian cinema chains into one normalized list.
Given a city, api_chains asks every chain that operates there for its current billboard,
merges the same film showing under slightly different titles into a single entry, and then
pulls per-theater showtimes for each film.
$ python orchestrator.py "Bogotá"
La Odisea [cine_col, cinemark, cinepolis, royal_films]
cine_col 2026-09-06 19:30 LUMINA
cinemark 2026-09-05 22:00 Pacific Mall
cinepolis 2026-09-05 21:10 Cinépolis VIP Plaza Claro DOB
Logos are hotlinked from Wikimedia Commons.
Requires Python 3.11+.
python -m venv env
source env/bin/activate
pip install -r requirements.txt
# Cine Colombia needs a real browser to capture its API token
playwright install firefoxpython orchestrator.py "<City>"- City name is the single argument. Quote names with spaces or accents:
"Santa Marta","Bogotá". - No argument → defaults to
Bogotá. - Unknown city → prints the list of supported cities and exits.
from orchestrator import list_films, get_showtimes
films = list_films("Cali") # -> list[Film]
for film in films:
for chain_data in get_showtimes("Cali", film): # -> list[ChainFilmData]
for s in chain_data.showtimes:
print(chain_data.chain, s.date, s.start, s.theater_name)get_showtimes accepts an optional on=datetime.date(...) to keep only showtimes on that day.
orchestrator.list_films(city)
│
├── chains_for_city(city) static_data/cities.py — which chains operate in this city
│
├── for each chain code:
│ scraper.fetch_films_raw(city) → raw API payload / HTML
│ scraper.parse_films(raw, city) → list[FilmScraped] (title, slug, chain)
│
└── merge_films(all_scraped) data_management/deduplication.py
fuzzy-matches titles (rapidfuzz token_set_ratio ≥ 90)
→ list[Film] (title, slugs={chain: slug}, chains=[...])
orchestrator.get_showtimes(city, film)
└── for each chain in film.chains:
scraper.fetch_showtimes_raw(city, film.slugs[chain])
scraper.parse_showtimes(raw, city, slug) → list[Showtime]
→ ChainFilmData(chain, showtimes, ticket_url)
Each chain scraper is a subclass of ChainScraper (chains/base.py) and registers itself
in CHAIN_REGISTRY on import. chains/__init__.py imports every chain module so the
registry is populated as a side effect.
| Path | Responsibility |
|---|---|
orchestrator.py |
Entry point + list_films / get_showtimes |
chains/base.py |
ChainScraper abstract base |
chains/<name>.py |
One scraper per chain, self-registering |
data_management/data_types.py |
FilmScraped, Showtime, Film, ChainFilmData dataclasses |
data_management/deduplication.py |
merge_films — fuzzy title grouping |
data_management/text_normalization.py |
Accent stripping, slugifying, match normalization |
static_data/cities.py |
Supported cities + city→chains map |
static_data/*_data.py |
Per-chain city/site ID lookup tables |
browser/browser.py |
Headless-browser auth-header capture (Playwright) for cine_col |
FilmScraped(title, slug, chain)
Showtime(city, theater_name, date, start, chain, format=None, language=None)
Film(title, slugs={chain: slug}, chains=[chain, ...])
ChainFilmData(chain, showtimes=[Showtime, ...], ticket_url=None)- Create
chains/mychain.pywith aChainScrapersubclass implementingfetch_films_raw,parse_films,fetch_showtimes_raw,parse_showtimes(and optionallyticket_url). - Call
_register(MyChain())at the bottom of the module. - Add
from chains import mychain as _mychaintochains/__init__.py. - Add the chain's city coverage to
_CHAIN_CITIESinstatic_data/cities.py.
- Sequential fetching. A large city fans out to one HTTP request per film per chain, so a
full run can take a couple of minutes. Parallelizing
get_showtimesis the obvious next step. merge_filmsgroups purely on title similarity; a very generic or heavily localized title can merge two different films or split one.