# ----------------------------------------------------------------------------- # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/mauricie.py : Tourisme Mauricie (ATR) — RÉHABILITÉE (vague ATR # 2026-08 ; écartée Phase 3 « liste rendue JS », l'endpoint XHR est percé) # Source : tourismemauricie.com/evenements — calendrier officiel de l'ATR # (thème WordPress « Acolyte », listing Vue.js). # Extraction: API JSON interne du thème (celle que le composant Vue appelle, # vérifiée 2026-08-25) : # GET /wp-json/acolyte/v1/get-events-data?lang=fr&page=N # → ~174 événements paginés 9/page : id, titre, url, images CDN, # ville, salle (venue_name), catégories, date FR, GPS. Fiches # détail incrémentales (cache data/mauricie_cache.json, plafond # MAU_FICHE_MAX/sync, re-visite 7 j) : JSON-LD Event → date de # début ISO, adresse civique, code postal, description. # Accès : API REST WordPress publique du thème (aucune auth), robots.txt # sans interdiction — GET throttlés, UA honnête. Ni prix ni heure # structurés publiés → jamais inventés. Région administrative # résolue par la VILLE (l'ATR couvre aussi des villes du # Centre-du-Québec : Bécancour, Nicolet…), repli « Mauricie ». # ----------------------------------------------------------------------------- from __future__ import annotations import json import os import re import time from ..normalize import clean_text, parse_date_range_fr from ..regions import region_from_city from ..schema import Event from .base import BaseConnector BASE = "https://tourismemauricie.com" API = BASE + "/wp-json/acolyte/v1/get-events-data" MAU_PAGE_MAX = int(os.environ.get("MAU_PAGE_MAX", "40")) MAU_FICHE_MAX = int(os.environ.get("MAU_FICHE_MAX", "12")) REFRESH_AFTER = 7 * 86400 _LDJSON_RE = re.compile( r'', re.S) def _ld_event(html: str) -> dict | None: for m in _LDJSON_RE.findall(html): try: data = json.loads(m) except ValueError: continue for item in (data if isinstance(data, list) else [data]): if isinstance(item, dict) and item.get("@type") == "Event": return item return None class MauricieConnector(BaseConnector): source_id = "mauricie" request_delay = 0.8 def _list_events(self) -> list[dict]: rows: list[dict] = [] page, total_pages = 1, 1 while page <= min(total_pages, MAU_PAGE_MAX): data = self.get_json(API, params={"lang": "fr", "page": page}) batch = data.get("events") or [] if not batch: break rows.extend(batch) total_pages = int(data.get("total_pages") or 1) page += 1 return rows def _parse_fiche(self, html: str) -> dict: """JSON-LD Event de la fiche : date ISO, adresse, description.""" ld = _ld_event(html) if not ld: return {} addr = ((ld.get("location") or {}).get("address") or {}) return { "start_date": (ld.get("startDate") or "")[:10], "end_date": (ld.get("endDate") or "")[:10], "description": clean_text(ld.get("description") or ""), "address": clean_text(addr.get("streetAddress") or ""), "city": clean_text(addr.get("addressLocality") or ""), "postal_code": (addr.get("postalCode") or "").strip(), } def fetch(self) -> list[Event]: rows = self._list_events() cache = self.load_cache() now = time.time() current = {str(r.get("id")) for r in rows} cache = {k: v for k, v in cache.items() if k in current} to_fetch = sorted( (r for r in rows if r.get("url") and (str(r.get("id")) not in cache or now - cache[str(r.get("id"))].get("ts", 0) > REFRESH_AFTER)), key=lambda r: cache.get(str(r.get("id")), {}).get("ts", 0) )[:MAU_FICHE_MAX] for row in to_fetch: eid = str(row.get("id")) detail: dict = {} try: detail = self._parse_fiche(self.get(row["url"]).text) except Exception: detail = {} cache[eid] = {"ts": now, "detail": detail} self.save_cache(cache) events: list[Event] = [] seen: set[str] = set() for r in rows: eid = str(r.get("id") or "") title = clean_text(r.get("title") or "") if not eid or eid in seen or not title: continue seen.add(eid) detail = cache.get(eid, {}).get("detail") or {} start, end = parse_date_range_fr(r.get("date") or "") pos = r.get("position") or {} city = clean_text(r.get("city") or "") or detail.get("city", "") cats = [clean_text(c.get("title", "")) for c in (r.get("categories") or []) if isinstance(c, dict)] # la source recopie parfois le nom de l'événement comme nom de # salle — on ne garde le lieu que s'il diffère du titre venue = clean_text(r.get("venue_name") or "") if venue.lower() == title.lower(): venue = "" events.append(Event( source=self.source_id, external_id=eid, url=r.get("url", ""), title=title, description=detail.get("description", ""), raw_categories=[c for c in cats if c] + [title], venue=venue, address=detail.get("address", ""), city=city, # l'ATR couvre des villes du Centre-du-Québec : la ville prime region=region_from_city(city), postal_code=detail.get("postal_code", ""), tourist_region="Mauricie", lat=pos.get("lat"), lng=pos.get("lng"), start_date=detail.get("start_date") or start, end_date=detail.get("end_date") or end, image=r.get("image_large") or r.get("image_medium") or r.get("image_small") or "", )) return events