SPB Git forge

spb/sorti-ka

Public

Toutes les sorties et tous les événements du Québec, un seul endroit — 7 connecteurs, fiches SSR, design Groupe KA.

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%
4.7 KB · 106 lines python
Raw Blame History
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