Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.
HTML 82.9%
Python 15.2%
TypeScript 0.9%
JavaScript 0.7%
1# ==============================================================================2# Author: Simon-Pierre Boucher <contact@spboucher.ai>3# File: src/main.py4# Desc: Acteur Apify ka-tuxedo — billetteries Tuxedo (*.tuxedobillet.com).5# Les tenants sont des SPA Angular sur Firebase (RTDB verrouillée,6# 401 même avec jeton anonyme — vérifié 2026-08-21) : le rendu7# navigateur est la seule voie. Playwright (Chromium) + proxy Apify8# (résidentiel CA par défaut) rend chaque tenant et extrait les tuiles9# .tuxCmsEdit__webShowTile : titre, sous-titre, date, image, lien10# /main/<slug>. Un item de dataset par spectacle.11# ==============================================================================12from __future__ import annotations1314import asyncio15from urllib.parse import urlparse1617from apify import Actor18from playwright.async_api import async_playwright1920CONCURRENCY = 4 # pages simultanées (29 tenants ≈ 6 vagues)212223def _pw_proxy(url: str | None) -> dict | None:24 """URL de proxy Apify → dict Playwright (identifiants SÉPARÉS du server —25 Playwright ignore les credentials inline, cause de timeouts silencieux)."""26 if not url:27 return None28 p = urlparse(url)29 proxy = {"server": f"{p.scheme}://{p.hostname}:{p.port}"}30 if p.username:31 proxy["username"] = p.username32 proxy["password"] = p.password or ""33 return proxy3435TILE = ".tuxCmsEdit__webShowTile"3637EXTRACT_JS = """38() => Array.from(document.querySelectorAll('.tuxCmsEdit__webShowTile')).map(t => {39 const q = (sel) => { const e = t.querySelector(sel); return e ? e.textContent.trim() : ''; };40 const a = t.querySelector('a[href^="/main/"]');41 const img = t.querySelector('img.tuxCmsEdit__webShowTile--showImage');42 return {43 slug: a ? a.getAttribute('href').replace('/main/', '') : '',44 title: q('.tuxCmsEdit__webShowTile--showTitle'),45 subtitle: q('.tuxCmsEdit__webShowTile--showSubtitle'),46 date_raw: q('.tuxCmsEdit__webShowTile--showDate'),47 image: img ? img.getAttribute('src') : '',48 };49})50"""515253async def main() -> None:54 async with Actor:55 inp = await Actor.get_input() or {}56 tenants = [t.strip().lower() for t in inp.get("tenants", []) if t.strip()]57 wait_ms = int(inp.get("renderWaitMs", 6000))58 proxy_cfg = await Actor.create_proxy_configuration(59 actor_proxy_input=inp.get("proxyConfiguration"))60 proxy_url = await proxy_cfg.new_url() if proxy_cfg else None6162 async with async_playwright() as pw:63 browser = await pw.chromium.launch(64 headless=True, proxy=_pw_proxy(proxy_url))65 context = await browser.new_context(66 locale="fr-CA", viewport={"width": 1366, "height": 900})67 sem = asyncio.Semaphore(CONCURRENCY)6869 async def scrape(tenant: str) -> None:70 url = f"https://{tenant}.tuxedobillet.com/"71 async with sem:72 page = await context.new_page()73 try:74 await page.goto(url, wait_until="domcontentloaded",75 timeout=45_000)76 try:77 await page.wait_for_selector(TILE, timeout=25_000)78 except Exception:79 Actor.log.warning(f"{tenant}: aucune tuile rendue")80 await page.wait_for_timeout(wait_ms)81 # défilement : certaines listes chargent paresseusement82 for _ in range(6):83 await page.mouse.wheel(0, 2400)84 await page.wait_for_timeout(300)85 shows = await page.evaluate(EXTRACT_JS)86 seen: set[str] = set()87 for s in shows:88 if (not s["title"] or not s["slug"]89 or s["slug"] in seen):90 continue91 seen.add(s["slug"])92 await Actor.push_data({93 "tenant": tenant,94 "url": (f"https://{tenant}.tuxedobillet.com"95 f"/main/{s['slug']}"),96 **s,97 })98 Actor.log.info(f"{tenant}: {len(seen)} spectacle(s)")99 except Exception as exc:100 Actor.log.exception(f"{tenant}: échec ({exc})")101 finally:102 await page.close()103104 await asyncio.gather(*(scrape(t) for t in tenants))105 await browser.close()106