尚未偵測到更新。GitHub 發布版本後會顯示在這裡。
個人 GitHub 帳號下公開儲存庫的擁有者可以登入並驗證所有權,自行管理介紹,無需管理員核准。
登入並產生驗證檔案,在 30 分鐘內將其提交至此儲存庫的預設分支。其他分支或分叉儲存庫中的檔案無效。然後在下方檢查檔案,即可無需管理員核准直接管理專案。請保留檔案以維持管理權限。
如需登入,將前往登入頁面。登入後請回到此處,再次點擊所選的驗證按鈕。
在專案資料夾中執行 /nomorevibe verify,即可驗證所有權並自行管理介紹。
一分鐘完成驗證What's playing on Buenos Aires indie screens?
翻譯準備中
取自儲存庫 README
正在載入完整 README。
檢視完整 README ↗Cartelera curada de Buenos Aires
afiche pulls the weekly programming of Buenos Aires' independent and repertory cinemas into one clean, editorial-feel cartelera — one screen for the whole indie circuit, built for people who already know the difference between a Saturday at Lugones and a Saturday at Hoyts. Live at afiche.ar — Next.js on Vercel, libSQL on Turso.
?ventana=hoy|finde|semana|prox (shareable; an unknown value falls back to hoy). A single-showtime film renders an inline time · venue; a multi-showtime film collapses to a {n} funciones · {venue} summary that taps open. Prod data earns the layout: 64% of films have one showtime all week and 95% play a single venue, so the common row is one clean line./cartelera. One tap from the homepage ("Ver todo →"): a 14-day rolling window of full cards grouped by day, a sticky date strip, and a Próximamente text index for the longer tail./pelicula/<slug> gives each film a title block, an optional 16:9 editorial still, poster, synopsis, top-billed cast — and the killer feature: every upcoming screening of that film across BA, in one list. It answers "when else can I catch this?", which no single venue's own site can./programa/<slug> pages for curated cycles (Lugones Olivera-Aries, MALBA Cineclub Nocturna). Cycles are denormalized text on screenings for now.| Cinema | Neighborhood | Type | Source |
|---|---|---|---|
| Sala Leopoldo Lugones | San Nicolás | indie / repertory | complejoteatral.gob.ar |
| MALBA | Palermo | indie / museum | malba.org.ar |
| Cine Lorca | San Nicolás | indie / repertory | cinelorca.wixsite.com (VLM-extracted poster) |
| Cine Cosmos | Balvanera | indie / repertory | cinecosmos.uba.ar |
| Cine York | Olivos | indie / repertory | lumiton.ar |
| Centro Cultural Munro | Munro | indie / community | lumiton.ar |
| Lumiton | Munro | indie / museum | lumiton.ar |
| Cine Gaumont | San Nicolás | indie / repertory (INCAA) | cinegaumont.ar (Cinexo ticketing JSON API) |
| CineArte Cacodelphia | San Nicolás | indie / repertory | cineartecacodelphia.com.ar (adro.studio JSON API) |
| Centro Cultural Borges | San Nicolás | indie / cultural center | centroculturalborges.gob.ar |
| Cineclub Lucero | Palermo | indie / cineclub | eventbrite.com (organizer feed) |
| Cinta | Palermo | indie / open-air terrace | passline.com (producer page) |
| El Cairo | Rosario | indie / public cinema (Santa Fe province) | elcairocinepublico.gob.ar (The Events Calendar REST API) |
graph LR
V["12 venue sites<br/>lugones · malba · gaumont<br/>lorca · cosmos · york · …"]
subgraph pipeline["Scrape pipeline · scrape-prod.sh (residential IP)"]
R["run.ts<br/>orchestrator"]
P["Provider.fetch() x12<br/>→ ScrapedScreening[]"]
I["ingest()<br/>upsert films · replace future<br/>screenings · enrich"]
end
TMDB["TMDB API<br/>match (similarity) + enrich"]
D[("Turso / SQLite · Drizzle<br/>cinemas · films · screenings<br/>providers · scrapeRuns<br/>tmdbOverrides · healRuns")]
subgraph web["Next.js 16 · RSC on Vercel"]
Q["queries.ts"]
PAGES["/ · /cartelera<br/>/pelicula/[slug] · /sala/[id]"]
ADMIN["/admin<br/>signed-cookie auth<br/>manual TMDB match"]
REV["/api/revalidate"]
end
U(("visitor"))
V -->|HTML / JSON| P
R -->|drives| P
P -->|screenings| I
I <-->|lookup + enrich| TMDB
I -->|upsert| D
R -.->|POST when done| REV
REV -.->|revalidate ISR| PAGES
D --> Q --> PAGES
ADMIN <-->|read / fix| D
U --> PAGES
Getting data in. The scrape doesn't run in the cloud. scrape-prod.sh runs on an always-on machine behind a residential connection, because lumiton.ar and complejoteatral.gob.ar 403 datacenter IPs and a GitHub Actions runner can't reach them. It's scheduled twice daily and pulls main before each run, so a shipped scraper fix reaches the pipeline without anyone deploying it by hand. It walks all 13 providers (src/scrapers/run.ts) and ingests each through src/scrapers/ingest.ts — upsert films → replace that cinema's future screenings → TMDB-enrich. Every provider is pure () → ScrapedScreening[]. Three of the thirteen (Cine York, Munro, Lumiton) share lumiton-agenda.ts because one Lumiton agenda page drives all three venues; Cine Lorca has no HTML schedule at all — its week lives on a single poster image, so its provider hashes the image and, on a cache miss, runs a Claude vision call (temperature: 0, structured JSON) to read the showtimes. Cinta has no site at all — its listing lives on a Passline producer page behind a fingerprint-based Cloudflare challenge, so its provider tries the origin first and falls back to a reader proxy that returns the same post-challenge markup, recording the fallback as a run warning.
Staying correct. Ingest is idempotent (unique index on (scraped_title, scraped_year)) and self-healing. A film that shows up with title drift across cinemas — different localizations, year-null collisions — collapses to one row keyed on tmdb_id after enrichment, and a title-ambiguity guard plus director verification stop silent wrong-matches on common titles (the Nosferatu class: Eggers 2024 vs Herzog 1979 vs Murnau 1922 all share a Spanish title). A failed match parks the row and stamps it with the matcher version that failed it (MATCHER_VERSION, src/tmdb/match.ts), so improving the matcher re-opens exactly the rows it was written to rescue — without that, a better matcher never gets a second look at its own backlog, because nothing about those rows changed. A pre-write circuit breaker guards the other direction: ingest replaces a venue's future schedule wholesale, so a provider that silently breaks against a site redesign would publish an empty venue — when the fetch comes back empty and future rows exist, the replace is refused, the last good schedule stands, and the run reports circuitBroke. Each run records one scrape_runs row per (cinema, run) with status + counts; when it finishes, scrape-prod.sh POSTs /api/revalidate (even when some providers failed, so the ones that landed still reach the site) so the deployed pages pick up the fresh data. A daily Vercel cron reads the newest successful run and warns if the cartelera has gone stale — the one failure a scraper can't report about itself is the run that never happened.
Repairing itself. Deterministic matching still misses, and until now the gap between a miss and a fix was however long it took someone to notice. A three-actor loop runs after every scrape, pass or fail (scripts/self-heal.ts, scripts/actor2-fix.ts). Actor 1 audits the run, judges the films still stuck against the candidates the matcher already fetched, and writes only what it can corroborate — a candidate-judged proposal at confidence ≥ 0.90 with the year or director agreeing, or at ≥ 0.95 when the title is an exact, dominant match nothing contradicts; a web-researched id never auto-applies. Applied matches land in tmdb_overrides (durable across reset-programming, and always outranked by the hand-curated tmdb-overrides.json seed) and re-open the film for the next enrich; everything else queues for a human. Misses that no single override fixes are grouped by cause and filed as matcher-pattern issues, and Actor 2 picks up the mechanical ones, prepares the fix in a throwaway worktree, and opens a PR — it never merges. Each write is recorded in heal_runs for a 7-day trend in the digest, and SELF_HEAL_ENABLED, SELF_HEAL_APPLY_ENABLED, LAYER2_ISSUES_ENABLED and ACTOR2_ENABLED pause any stage without a code change. Full walkthrough in docs/agents/self-heal-loop.md.
Getting data out. Every serving route — and the MCP server — reads the same DB through src/db/queries.ts. The homepage (src/app/page.tsx) calls getWindowScreeningsByFilm (one row per film, bounded by ?ventana=), getFeaturedFilms (the Destacados band), and getJsonLdScreenings (structured-data feed). /cartelera (src/app/cartelera/page.tsx) calls getTwoWeeksScreenings (the 14-day window), getUpcomingScreenings (Próximamente), and getLastScreeningPerFilm (the ÚLTIMA FUNCIÓN anchor). /pelicula/[slug] calls getUpcomingScreeningsByFilm for the cross-venue list. The window registry (src/lib/windows.ts) is the single source for the nav, the ?ventana= validation, and the bounded query.
The cartelera as data. /api/mcp exposes the same live cartelera over the Model Context Protocol (Streamable HTTP, stateless), so any MCP client can answer "¿qué dan esta noche en Palermo?" against real showtimes. Four read-only tools — search_films, whats_on, get_film, list_cinemas — wrap the query layer in src/mcp/, return validated structuredContent, and emit Buenos Aires local times. Every showtime it advertises is filtered to still-catchable, so it never sends anyone to a screening that already started. No credentials; it's the same public data the site renders. The reading pages meet agents halfway too: /, /cartelera and /acerca answer an Accept: text/markdown request with markdown at the same public URL, and /llms.txt describes the site and how to call the MCP tools.
claude mcp add --transport http afiche https://afiche.ar/api/mcp
force-dynamic on the cartelera + film pages@theme (see DESIGN.md)claude-sonnet-4-6) for Cine Lorca's image-only poster, image-hash cachedmcp-handler + @modelcontextprotocol/sdk serving the read-only cartelera tools at /api/mcpafiche uses the TMDB API but is not endorsed or certified by TMDB. TMDB provides the film metadata, poster images, and editorial stills rendered alongside each screening. afiche hotlinks TMDB's CDN (image.tmdb.org/t/p/...) rather than re-hosting, per TMDB's best-practice guidance, and never resells or commercially redistributes TMDB data. Cine Lorca's image-only weekly programming is read via Anthropic's Claude vision API; the image is hashed and the result cached locally, so a given poster is parsed at most once.
Licensed under the PolyForm Noncommercial License 1.0.0 (source-available). Noncommercial use is permitted; commercial use is reserved to the author.
Built by Benjamin Delasoie. Spot a screening afiche missed or got wrong? Issues welcome.