SPB Git

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.

Vague 3 : Ticketmaster SANS clé + Eventbrite + Longueuil + Cantons-de-l'Est

- ticketmaster : mode principal sans clé — scraping des pages découverte
  publiques (montreal, quebec-city, gatineau ; état Next.js cityEvents,
  20 év./page, date UTC→locale, salle géolocalisée, plafond 360/sync,
  Scrapfly ASP sans render_js en secours) ; repli automatique sur la
  Discovery API officielle dès que TICKETMASTER_API_KEY existe (mêmes ids,
  zéro doublon). 330 événements, 100 % GPS, 96 % heure.
- eventbrite (nouveau) : pages découverte /d/<lieu>/all-events (7 villes QC,
  __SERVER_DATA__, 20 rés./page) + prix réels par fiche JSON-LD
  AggregateOffer (cache incrémental, 150 fiches/sync, re-visite 7 j).
  426 événements, 100 % GPS+heure, filtre region=QC strict.
- longueuil (nouveau) : portail vivant longueuil.quebec/fr/evenements
  (cartes SSR <time datetime> + adresse par fiche via l état Nuxt) —
  remplace le jeu Données Québec périmé (2017).
- cantonsdelest (nouveau) : calendrier officiel de l ATR Estrie
  (JSON-LD Event + GPS data-lat/lng, images Tourinsoft, cache incrémental).
- tests : parse du mode scraping Ticketmaster (UTC→locale, dédup par id,
  annulés exclus, heure TBA nulle) ; 33 tests verts.
- correctif commun : raw_decode tolère l espace avant le JSON-LD.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed (Aug 18, 2026) parent 332cc93 Browse files

27 changed files +1,224 −232

modified data/sources.json +38 −16
@@ -58,15 +58,37 @@
58 }, 58 },
59 { 59 {
60 "id": "longueuil", 60 "id": "longueuil",
61− "name": "Ville de Longueuil — Événements", 61+ "name": "Ville de Longueuil — Événements (nouveau portail)",
62− "url": "https://www.donneesquebec.ca/recherche/dataset/evenements", 62+ "url": "https://longueuil.quebec/fr/evenements",
63− "type": "donnees-ouvertes", 63+ "type": "municipal",
64− "extraction": "CSV (Données Québec)", 64+ "extraction": "page liste rendue serveur (portail Nuxt) — cartes <article class=\"event__item\"> : <time datetime> (date + heure locales, début/fin), titre, lien, extrait ; enrichissement par fiche incrémental (cache data/longueuil_cache.json, 30 fiches/sync, re-visite 30 j) : adresse civique, code postal, municipalité depuis l'état Nuxt (address_line1/postal_code/locality) ; GPS via le géocodeur central",
65− "acces_legal": "Licence CC 4.0 (Données Québec)", 65+ "acces_legal": "portail municipal public, rendu serveur — GET throttlés 0,8 s, identification honnête",
66− "couverture": "Longueuil (Montérégie)", 66+ "couverture": "Événements municipaux, Longueuil (Montérégie)",
67− "cadence": "n/d", 67+ "cadence": "horaire (watcher PM2, incrémental)",
68− "statut": "abandonnee", 68+ "statut": "actif",
69− "note": "Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17)" 69+ "note": "Remplace le jeu Données Québec « evenements » périmé (dernière date 2017-03, vérifié 2026-08-17) par le portail vivant longueuil.quebec."
70+ },
71+ {
72+ "id": "eventbrite",
73+ "name": "Eventbrite — billetterie libre-service (Québec)",
74+ "url": "https://www.eventbrite.ca/d/canada--montreal/all-events/",
75+ "type": "billetterie",
76+ "extraction": "pages de découverte publiques /d/<lieu>/all-events/?page=N (7 lieux QC pondérés : Montréal, Québec, Laval, Gatineau, Sherbrooke, Longueuil, Trois-Rivières) — window.__SERVER_DATA__ livre 20 résultats/page : nom, résumé, date + heure locales, salle géocodée (lat/lng, adresse, code postal), image, catégories (tags Eventbrite*), URL ; prix par fiche détail /e/<slug> incrémentale (JSON-LD AggregateOffer lowPrice/highPrice CAD ; cache data/eventbrite_cache.json, 150 fiches/sync, re-visite 7 j) ; en ligne exclus, filtre region=QC strict",
77+ "acces_legal": "robots.txt « User-agent: * » n'interdit ni /d/ ni /e/ (seuls /directory/, checkout et endpoints privés) — GET throttlés 0,8 s, identification honnête, lien direct vers chaque fiche",
78+ "couverture": "Événements communautaires, ateliers, festivals, affaires — grandes villes du Québec",
79+ "cadence": "horaire (watcher PM2, incrémental)",
80+ "statut": "actif"
81+ },
82+ {
83+ "id": "cantonsdelest",
84+ "name": "Tourisme Cantons-de-l'Est — calendrier officiel (ATR Estrie)",
85+ "url": "https://www.cantonsdelest.com/evenements",
86+ "type": "tourisme-regional",
87+ "extraction": "page liste rendue serveur (~25 fiches à venir) puis fiches détail incrémentales (cache data/cantonsdelest_cache.json, 40 fiches/sync, re-visite 7 j) : JSON-LD Event complet (nom, dates, description riche, adresse civique, images Tourinsoft mto.media.tourinsoft.eu) + GPS exact (data-lat/data-lng de la carte) ; même plateforme Tourinsoft que le SIT Québec → recoupement géré par dedup_key",
88+ "acces_legal": "pages publiques de l'ATR, rendu serveur — GET throttlés 0,8 s, identification honnête, lien vers la fiche originale",
89+ "couverture": "Festivals, spectacles, événements — Cantons-de-l'Est (Estrie)",
90+ "cadence": "horaire (watcher PM2, incrémental)",
91+ "statut": "actif"
70 }, 92 },
71 { 93 {
72 "id": "saguenay", 94 "id": "saguenay",
@@ -141,12 +163,12 @@
141 "name": "Ticketmaster — Discovery API (Québec)", 163 "name": "Ticketmaster — Discovery API (Québec)",
142 "url": "https://developer.ticketmaster.com/products-and-docs/apis/discovery-api/v2/", 164 "url": "https://developer.ticketmaster.com/products-and-docs/apis/discovery-api/v2/",
143 "type": "billetterie", 165 "type": "billetterie",
144− "extraction": "API officielle Discovery v2 — GET JSON paginé (countryCode=CA & stateCode=QC, size=200, tri par date) : nom, dates + heure locale, salle géolocalisée (venues lat/lng), fourchette de prix (priceRanges min/max), images 16:9, classifications → catégories, attractions → artistes, URL de billetterie officielle ; throttle 0,25 s (quota 5 req/s)", 166+ "extraction": "SANS clé (mode par défaut) : scraping léger des pages découverte publiques www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city, gatineau) — l'état Next.js (__NEXT_DATA__, requête serveur cityEvents) livre 20 événements/page : titre, date UTC + fuseau → date/heure locales, salle géolocalisée (lat/lng), adresse, code postal, ville, URL billetterie, catégorie majeure, plan de salle (image) ; plafonds TM_MAX_EVENTS=360 / TM_PAGES_PER_CITY=8 ; Scrapfly ASP sans render_js en secours anti-bot ; aucun prix publié dans ce flux → jamais inventé. REPLI AUTOMATIQUE : si TICKETMASTER_API_KEY apparaît dans .env, bascule sur la Discovery API v2 officielle (priceRanges, attractions → artistes, images 16:9) — identifiants identiques, zéro doublon.",
145− "acces_legal": "API officielle publique avec clé gratuite (developer.ticketmaster.com) — conditions Ticketmaster Developer respectées", 167+ "acces_legal": "pages découverte publiques rendues serveur — robots.txt n'interdit pas /discover/ (seuls /api/ismds/*) ; GET throttlés 1 s, lien direct vers la billetterie officielle ; API officielle (clé gratuite) utilisée dès qu'une clé est fournie",
146− "couverture": "Grands spectacles, sports et concerts — partout au Québec", 168+ "couverture": "Grands spectacles, sports et concerts — Montréal/Laval/Brossard, Québec, Gatineau",
147− "cadence": "horaire (watcher PM2) dès que la clé est fournie", 169+ "cadence": "horaire (watcher PM2)",
148− "statut": "prêt — clé requise", 170+ "statut": "actif",
149− "note": "Nécessite TICKETMASTER_API_KEY dans .env (clé gratuite). Sans clé, le connecteur s'annonce « clé manquante » et ne retourne rien." 171+ "note": "Fonctionne sans clé (scraping découverte). TICKETMASTER_API_KEY optionnelle : active le mode API riche (prix, artistes, images)."
150 }, 172 },
151 { 173 {
152 "id": "bandsintown", 174 "id": "bandsintown",
@@ -208,4 +230,4 @@
208 "note": "Cloudflare bloque tout GET non navigateur (403) et aucun jeu « événements » sur Données Québec (vérifié 2026-08-18)" 230 "note": "Cloudflare bloque tout GET non navigateur (403) et aucun jeu « événements » sur Données Québec (vérifié 2026-08-18)"
209 } 231 }
210 ] 232 ]
211−}
\ No newline at end of file
233+}
modified docs/connecteurs/INDEX.md +16 −14
@@ -1,8 +1,8 @@
1 # Sorti-Ka — Connecteurs de sources d'événements 1 # Sorti-Ka — Connecteurs de sources d'événements
2 2
3−> Index généré le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` (rejouable : `python3 scripts/gen_connector_docs.py`). Croise `data/sources.json`, l'introspection de `sortika/connectors/*.py` et la base live `data/sortika.db`. 3+> Index généré le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` (rejouable : `python3 scripts/gen_connector_docs.py`). Croise `data/sources.json`, l'introspection de `sortika/connectors/*.py` et la base live `data/sortika.db`.
4 4
5−**18 sources au registre** — 11 actives/prêtes, 7 abandonnées · **18292 événements en base** (dont 15820 actifs). 5+**20 sources au registre** — 14 actives/prêtes, 6 abandonnées · **19193 événements en base** (dont 16719 actifs).
6 6
7 ⚠️ **Attributions requises** : Ville de Montréal (CC BY 4.0) ; SIT Québec / Ministère du Tourisme et Ville de Laval (CC 4.0 — Données Québec). Détails dans chaque fiche, section « Licence / attribution ». 7 ⚠️ **Attributions requises** : Ville de Montréal (CC BY 4.0) ; SIT Québec / Ministère du Tourisme et Ville de Laval (CC 4.0 — Données Québec). Détails dans chaque fiche, section « Licence / attribution ».
8 8
@@ -10,23 +10,25 @@
10 10
11 | Source | Type | Statut | Actifs | GPS | Prix | Heure | Artistes | Image | Salle | Dernier sync | 11 | Source | Type | Statut | Actifs | GPS | Prix | Heure | Artistes | Image | Salle | Dernier sync |
12 |---|---|---|---|---|---|---|---|---|---|---| 12 |---|---|---|---|---|---|---|---|---|---|---|
13−| [SIT Québec — Ministère du Tourisme](sitq.md) | donnees-ouvertes | actif | 1598 | 97 % | 0 % | 0 % | 0 % | 35 % | 66 % | 2026-08-18 02:41 | 13+| [SIT Québec — Ministère du Tourisme](sitq.md) | donnees-ouvertes | actif | 1598 | 97 % | 0 % | 0 % | 0 % | 35 % | 66 % | 2026-08-18 05:01 |
14−| [Ville de Montréal — Événements publics](montreal.md) | donnees-ouvertes | actif | 5679 | 97 % | 100 % | 10 % | 0 % | 58 % | 4 % | 2026-08-18 02:41 | 14+| [Ville de Montréal — Événements publics](montreal.md) | donnees-ouvertes | actif | 5679 | 97 % | 100 % | 21 % | 0 % | 58 % | 4 % | 2026-08-18 05:01 |
15−| [Le point de vente — billetterie québécoise](lepointdevente.md) | billetterie | actif | 1787 | 27 % | 31 % | 94 % | 1 % | 100 % | 99 % | 2026-08-18 02:41 | 15+| [Le point de vente — billetterie québécoise](lepointdevente.md) | billetterie | actif | 1787 | 27 % | 46 % | 94 % | 1 % | 100 % | 99 % | 2026-08-18 05:00 |
16−| [Ville de Laval — Calendrier des activités](laval.md) | donnees-ouvertes | actif | 277 | 51 % | 0 % | 95 % | 0 % | 98 % | 90 % | 2026-08-18 02:35 | 16+| [Ville de Laval — Calendrier des activités](laval.md) | donnees-ouvertes | actif | 277 | 51 % | 0 % | 95 % | 0 % | 98 % | 90 % | 2026-08-18 04:55 |
17−| [Ville de Sherbrooke — Calendrier des événements](sherbrooke.md) | municipal | actif | 271 | 0 % | 100 % | 88 % | 0 % | 100 % | 0 % | 2026-08-18 02:41 | 17+| [Ville de Sherbrooke — Calendrier des événements](sherbrooke.md) | municipal | actif | 271 | 0 % | 100 % | 88 % | 0 % | 100 % | 0 % | 2026-08-18 05:01 |
18−| [evenko — grands concerts et festivals](evenko.md) | promoteur | actif | 423 | 77 % | 0 % | 99 % | 99 % | 100 % | 100 % | 2026-08-18 02:35 | 18+| [Ville de Longueuil — Événements (nouveau portail)](longueuil.md) | municipal | actif | 9 | 0 % | 0 % | 100 % | 0 % | 0 % | 0 % | 2026-08-18 05:52 |
19−| [Atuvu.ca — calendrier culturel](atuvu.md) | agregateur | actif | 145 | 62 % | 35 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 02:31 | 19+| [Eventbrite — billetterie libre-service (Québec)](eventbrite.md) | billetterie | actif | 426 | 100 % | 65 % | 100 % | 0 % | 97 % | 100 % | 2026-08-18 05:52 |
20−| [La Vitrine culturelle](lavitrine.md) | billetterie | actif | 5127 | 52 % | 0 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 02:35 | 20+| [Tourisme Cantons-de-l'Est — calendrier officiel (ATR Estrie)](cantonsdelest.md) | tourisme-regional | actif | 13 | 77 % | 0 % | 0 % | 0 % | 100 % | 0 % | 2026-08-18 05:52 |
21−| [Ville de Brossard — Calendrier des événements](brossard.md) | municipal | actif | 262 | 0 % | 0 % | 54 % | 0 % | 55 % | 53 % | 2026-08-18 02:35 | 21+| [evenko — grands concerts et festivals](evenko.md) | promoteur | actif | 421 | 77 % | 0 % | 99 % | 99 % | 100 % | 100 % | 2026-08-18 04:55 |
22−| [Ticketmaster — Discovery API (Québec)](ticketmaster.md) | billetterie | prêt — clé requise | 0 | — | — | — | — | — | — | 2026-08-18 02:41 | 22+| [Atuvu.ca — calendrier culturel](atuvu.md) | agregateur | actif | 145 | 62 % | 35 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 04:52 |
23−| [Bandsintown — dates de concerts par artiste](bandsintown.md) | agregateur | actif | 251 | 100 % | 1 % | 100 % | 100 % | 10 % | 100 % | 2026-08-18 02:32 | 23+| [La Vitrine culturelle](lavitrine.md) | billetterie | actif | 5127 | 52 % | 0 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 04:55 |
24+| [Ville de Brossard — Calendrier des événements](brossard.md) | municipal | actif | 264 | 0 % | 0 % | 55 % | 0 % | 57 % | 54 % | 2026-08-18 04:55 |
25+| [Ticketmaster — Discovery API (Québec)](ticketmaster.md) | billetterie | actif | 330 | 100 % | 0 % | 96 % | 0 % | 90 % | 100 % | 2026-08-18 05:51 |
26+| [Bandsintown — dates de concerts par artiste](bandsintown.md) | agregateur | actif | 372 | 100 % | 1 % | 100 % | 100 % | 8 % | 100 % | 2026-08-18 04:52 |
24 27
25 ## Sources abandonnées 28 ## Sources abandonnées
26 29
27 | Source | Type | Raison | 30 | Source | Type | Raison |
28 |---|---|---| 31 |---|---|---|
29−| [Ville de Longueuil — Événements](longueuil.md) | donnees-ouvertes | Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17) |
30 | [Ville de Saguenay — Événements culturels](saguenay.md) | donnees-ouvertes | Jeu de données périmé — dernière date d'événement : 2022-06 (vérifié 2026-08-17) | 32 | [Ville de Saguenay — Événements culturels](saguenay.md) | donnees-ouvertes | Jeu de données périmé — dernière date d'événement : 2022-06 (vérifié 2026-08-17) |
31 | [Ville de Repentigny — Événements](repentigny.md) | donnees-ouvertes | Jeu de données périmé — dernière date d'événement : 2021-09 (vérifié 2026-08-17) | 33 | [Ville de Repentigny — Événements](repentigny.md) | donnees-ouvertes | Jeu de données périmé — dernière date d'événement : 2021-09 (vérifié 2026-08-17) |
32 | [Ville de Gatineau — Événements](gatineau.md) | municipal | Aucun jeu de données « événements » sur Données Québec (portail Gatineau : géomatique seulement)… | 34 | [Ville de Gatineau — Événements](gatineau.md) | municipal | Aucun jeu de données « événements » sur Données Québec (portail Gatineau : géomatique seulement)… |
modified docs/connecteurs/atuvu.md +16 −16
@@ -1,6 +1,6 @@
1 # Atuvu.ca — calendrier culturel (`atuvu`) 1 # Atuvu.ca — calendrier culturel (`atuvu`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/atuvu.py` 5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/atuvu.py`
6 6
@@ -38,28 +38,28 @@
38 38
39 ## Champs récupérés → schéma 39 ## Champs récupérés → schéma
40 40
41−Complétude mesurée sur les **145 événements actifs** de la source ; exemples tirés d'un événement réel (`atuvu:requiem-de-mozart-2026-11-07`). 41+Complétude mesurée sur les **145 événements actifs** de la source ; exemples tirés d'un événement réel (`atuvu:lhorizon-de-kheops-2026-08-12`).
42 42
43 | Champ (`events.*`) | Description | Complétude | Exemple réel | 43 | Champ (`events.*`) | Description | Complétude | Exemple réel |
44 |---|---|---|---| 44 |---|---|---|---|
45−| `title` — Titre | titre de l'événement | 100 % | Requiem de Mozart | 45+| `title` — Titre | titre de l'événement | 100 % | L'Horizon de Khéops |
46 | `description` — Description | texte descriptif (source ou fiche détail) | 0 % | ∅ | 46 | `description` — Description | texte descriptif (source ou fiche détail) | 0 % | ∅ |
47−| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://atuvu.ca/billets-spectacle/requiem-de-mozart | 47+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://atuvu.ca/billets-spectacle/lhorizon-de-kheops |
48 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] | 48 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
49−| `venue` — Salle (venue) | nom du lieu | 100 % | Basilique Notre-Dame de Montréal | 49+| `venue` — Salle (venue) | nom du lieu | 100 % | Laurier Québec |
50 | `address` — Adresse | adresse civique | 0 % | ∅ | 50 | `address` — Adresse | adresse civique | 0 % | ∅ |
51−| `city` — Ville | municipalité | 97 % | Montréal | 51+| `city` — Ville | municipalité | 97 % | Québec |
52−| `region` — Région | région administrative | 93 % | Montréal | 52+| `region` — Région | région administrative | 93 % | Capitale-Nationale |
53−| `lat` — GPS (lat/lng) | coordonnées géographiques | 62 % | 45.5044365, -73.5559901 | 53+| `lat` — GPS (lat/lng) | coordonnées géographiques | 62 % | 46.7705941, -71.2847897 |
54−| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-11-07 | 54+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-12 |
55−| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-11-07 | 55+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-12 |
56 | `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 0 % | ∅ | 56 | `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 0 % | ∅ |
57 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 29 % | 0 | 57 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 29 % | 0 |
58−| `price_min` — Prix min | tarif minimal réel ($) | 35 % | 54.58 | 58+| `price_min` — Prix min | tarif minimal réel ($) | 35 % | 47.66 |
59−| `price_label` — Libellé prix | étiquette tarifaire affichable | 29 % | 54.58 $ (prix régulier atuvu.ca) | 59+| `price_label` — Libellé prix | étiquette tarifaire affichable | 29 % | 47.66 $ (prix régulier atuvu.ca) |
60 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] | 60 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
61 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ | 61 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
62−| `image` — Image | visuel (og:image ou média source) | 100 % | https://atuvu.ca/uploads/images/2026/08/requiem-mozart.jpg | 62+| `image` — Image | visuel (og:image ou média source) | 100 % | https://atuvu.ca/uploads/images/2025/12/lhorizon-kheops.jpg |
63 | `website` — Site web | site officiel de l'événement | 0 % | ∅ | 63 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
64 64
65 ## Fréquence & budgets 65 ## Fréquence & budgets
@@ -74,7 +74,7 @@ Complétude mesurée sur les **145 événements actifs** de la source ; exemples
74 74
75 ## Volumétrie & complétude live 75 ## Volumétrie & complétude live
76 76
77−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 77+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
78 78
79 - **Événements en base** : 145 (dont **145 actifs**) 79 - **Événements en base** : 145 (dont **145 actifs**)
80 - **GPS** : 90/145 actifs (62 %) 80 - **GPS** : 90/145 actifs (62 %)
@@ -83,8 +83,8 @@ Complétude mesurée sur les **145 événements actifs** de la source ; exemples
83 - **Artistes** : 0/145 actifs (0 %) 83 - **Artistes** : 0/145 actifs (0 %)
84 - **Image** : 145/145 actifs (100 %) 84 - **Image** : 145/145 actifs (100 %)
85 - **Salle (venue)** : 145/145 actifs (100 %) 85 - **Salle (venue)** : 145/145 actifs (100 %)
86−- **Dernier sync** : 2026-08-18 02:31 — fetched 156, +0 ajoutés, ~146 mis à jour, -0 retirés 86+- **Dernier sync** : 2026-08-18 04:52 — fetched 156, +0 ajoutés, ~2 mis à jour, -0 retirés
87−- **Syncs enregistrés** (sync_log) : 16 87+- **Syncs enregistrés** (sync_log) : 18
88 88
89 ## Erreurs & dépannage 89 ## Erreurs & dépannage
90 90
modified docs/connecteurs/bandsintown.md +24 −24
@@ -1,6 +1,6 @@
1 # Bandsintown — dates de concerts par artiste (`bandsintown`) 1 # Bandsintown — dates de concerts par artiste (`bandsintown`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/bandsintown.py` 5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/bandsintown.py`
6 6
@@ -43,28 +43,28 @@
43 43
44 ## Champs récupérés → schéma 44 ## Champs récupérés → schéma
45 45
46−Complétude mesurée sur les **251 événements actifs** de la source ; exemples tirés d'un événement réel (`bandsintown:108747079`). 46+Complétude mesurée sur les **372 événements actifs** de la source ; exemples tirés d'un événement réel (`bandsintown:108509738`).
47 47
48 | Champ (`events.*`) | Description | Complétude | Exemple réel | 48 | Champ (`events.*`) | Description | Complétude | Exemple réel |
49 |---|---|---|---| 49 |---|---|---|---|
50−| `title` — Titre | titre de l'événement | 100 % | Doug St-Louis & Dj Chuck / Ptit Gros Bison à Grenville en Fête | 50+| `title` — Titre | titre de l'événement | 100 % | Saint-Roch-de-l'Achigan en Fête 2026 |
51−| `description` — Description | texte descriptif (source ou fiche détail) | 18 % | Au Parc Normand-Woodbury à Grenville. Spectacle Gratuit Ils seront en duo pour vous faire danser… | 51+| `description` — Description | texte descriptif (source ou fiche détail) | 17 % | ∅ |
52−| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://www.bandsintown.com/e/108747079?app_id=js_www.sorti-ka.com&came_from=267&utm_medium=api&… | 52+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://www.bandsintown.com/e/108509738?app_id=js_www.sorti-ka.com&came_from=267&utm_medium=api&… |
53 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] | 53 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
54−| `venue` — Salle (venue) | nom du lieu | 100 % | Doug St-Louis & Dj Chuck / Ptit Gros Bison à Grenville en Fête | 54+| `venue` — Salle (venue) | nom du lieu | 100 % | Saint-Roch-de-l'Achigan en Fête 2026 |
55−| `address` — Adresse | adresse civique | 98 % | 21 Rue Tri Jean | 55+| `address` — Adresse | adresse civique | 98 % | Rue Vézina |
56−| `city` — Ville | municipalité | 100 % | Grenville | 56+| `city` — Ville | municipalité | 100 % | Saint-roch-de-l'achigan |
57−| `region` — Région | région administrative | 98 % | Laurentides | 57+| `region` — Région | région administrative | 98 % | Lanaudière |
58−| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.630063, -74.600568 | 58+| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.860801, -73.593176 |
59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-22 | 59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-22 |
60 | `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-22 | 60 | `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-22 |
61−| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 100 % | 20:30 | 61+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 100 % | 18:30 |
62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 1 % | 1 | 62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 1 % | 1 |
63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ | 63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
64 | `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ | 64 | `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
65−| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 100 % | ["Doug St-Louis", "P'tit Gros Bison"] | 65+| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 100 % | ["Laurence St-Martin"] |
66 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ | 66 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
67−| `image` — Image | visuel (og:image ou média source) | 10 % | https://photos.bandsintown.com/large/25853181.jpeg | 67+| `image` — Image | visuel (og:image ou média source) | 8 % | https://photos.bandsintown.com/large/17506085.jpeg |
68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ | 68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
69 69
70 ## Fréquence & budgets 70 ## Fréquence & budgets
@@ -80,17 +80,17 @@ Complétude mesurée sur les **251 événements actifs** de la source ; exemples
80 80
81 ## Volumétrie & complétude live 81 ## Volumétrie & complétude live
82 82
83−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 83+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
84− 84+
85−- **Événements en base** : 251 (dont **251 actifs**) 85+- **Événements en base** : 372 (dont **372 actifs**)
86−- **GPS** : 251/251 actifs (100 %) 86+- **GPS** : 372/372 actifs (100 %)
87−- **Prix** : 2/251 actifs (1 %) 87+- **Prix** : 4/372 actifs (1 %)
88−- **Heure (start_time)** : 250/251 actifs (100 %) 88+- **Heure (start_time)** : 371/372 actifs (100 %)
89−- **Artistes** : 251/251 actifs (100 %) 89+- **Artistes** : 372/372 actifs (100 %)
90−- **Image** : 24/251 actifs (10 %) 90+- **Image** : 29/372 actifs (8 %)
91−- **Salle (venue)** : 251/251 actifs (100 %) 91+- **Salle (venue)** : 372/372 actifs (100 %)
92−- **Dernier sync** : 2026-08-18 02:32 — fetched 251, +143 ajoutés, ~0 mis à jour, -0 retirés 92+- **Dernier sync** : 2026-08-18 04:52 — fetched 372, +0 ajoutés, ~0 mis à jour, -0 retirés
93−- **Syncs enregistrés** (sync_log) : 2 93+- **Syncs enregistrés** (sync_log) : 4
94 94
95 ## Erreurs & dépannage 95 ## Erreurs & dépannage
96 96
modified docs/connecteurs/brossard.md +17 −17
@@ -1,6 +1,6 @@
1 # Ville de Brossard — Calendrier des événements (`brossard`) 1 # Ville de Brossard — Calendrier des événements (`brossard`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/brossard.py` 5 **Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/brossard.py`
6 6
@@ -43,7 +43,7 @@
43 43
44 ## Champs récupérés → schéma 44 ## Champs récupérés → schéma
45 45
46−Complétude mesurée sur les **262 événements actifs** de la source ; exemples tirés d'un événement réel (`brossard:37979`). 46+Complétude mesurée sur les **264 événements actifs** de la source ; exemples tirés d'un événement réel (`brossard:37979`).
47 47
48 | Champ (`events.*`) | Description | Complétude | Exemple réel | 48 | Champ (`events.*`) | Description | Complétude | Exemple réel |
49 |---|---|---|---| 49 |---|---|---|---|
@@ -51,20 +51,20 @@ Complétude mesurée sur les **262 événements actifs** de la source ; exemples
51 | `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Dans le cadre des Journées de la culture, viens créer ton propre bracelet d’amitié ! Choisis par… | 51 | `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Dans le cadre des Journées de la culture, viens créer ton propre bracelet d’amitié ! Choisis par… |
52 | `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://brossard.ca/calendrier-evenements/atelier-libre-cree-ton-bracelet-damitie-26-septembre-2… | 52 | `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://brossard.ca/calendrier-evenements/atelier-libre-cree-ton-bracelet-damitie-26-septembre-2… |
53 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["conference"] | 53 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["conference"] |
54−| `venue` — Salle (venue) | nom du lieu | 53 % | Bibliothèque de Brossard Georgette-Lepage | 54+| `venue` — Salle (venue) | nom du lieu | 54 % | Bibliothèque de Brossard Georgette-Lepage |
55−| `address` — Adresse | adresse civique | 53 % | 7855, av San Francisco | 55+| `address` — Adresse | adresse civique | 54 % | 7855, av San Francisco |
56 | `city` — Ville | municipalité | 100 % | Brossard | 56 | `city` — Ville | municipalité | 100 % | Brossard |
57 | `region` — Région | région administrative | 100 % | Montérégie | 57 | `region` — Région | région administrative | 100 % | Montérégie |
58 | `lat` — GPS (lat/lng) | coordonnées géographiques | 0 % | 45.4551469, -73.4682277 | 58 | `lat` — GPS (lat/lng) | coordonnées géographiques | 0 % | 45.4551469, -73.4682277 |
59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-26 | 59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-26 |
60 | `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-26 | 60 | `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-26 |
61−| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 54 % | 13:00 | 61+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 55 % | 13:00 |
62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ | 62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ |
63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ | 63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
64 | `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ | 64 | `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
65 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] | 65 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
66 | `organizer` — Organisateur | promoteur / organisme | 100 % | Ville de Brossard | 66 | `organizer` — Organisateur | promoteur / organisme | 100 % | Ville de Brossard |
67−| `image` — Image | visuel (og:image ou média source) | 55 % | https://biblio.brossard.ca/wp-content/uploads/2026/07/bracelets_amitie.jpg | 67+| `image` — Image | visuel (og:image ou média source) | 57 % | https://biblio.brossard.ca/wp-content/uploads/2026/07/bracelets_amitie.jpg |
68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ | 68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
69 69
70 ## Fréquence & budgets 70 ## Fréquence & budgets
@@ -79,17 +79,17 @@ Complétude mesurée sur les **262 événements actifs** de la source ; exemples
79 79
80 ## Volumétrie & complétude live 80 ## Volumétrie & complétude live
81 81
82−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 82+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
83− 83+
84−- **Événements en base** : 262 (dont **262 actifs**) 84+- **Événements en base** : 264 (dont **264 actifs**)
85−- **GPS** : 1/262 actifs (0 %) 85+- **GPS** : 1/264 actifs (0 %)
86−- **Prix** : 0/262 actifs (0 %) 86+- **Prix** : 0/264 actifs (0 %)
87−- **Heure (start_time)** : 141/262 actifs (54 %) 87+- **Heure (start_time)** : 145/264 actifs (55 %)
88−- **Artistes** : 0/262 actifs (0 %) 88+- **Artistes** : 0/264 actifs (0 %)
89−- **Image** : 145/262 actifs (55 %) 89+- **Image** : 151/264 actifs (57 %)
90−- **Salle (venue)** : 138/262 actifs (53 %) 90+- **Salle (venue)** : 142/264 actifs (54 %)
91−- **Dernier sync** : 2026-08-18 02:35 — fetched 262, +11 ajoutés, ~56 mis à jour, -0 retirés 91+- **Dernier sync** : 2026-08-18 04:55 — fetched 264, +2 ajoutés, ~0 mis à jour, -0 retirés
92−- **Syncs enregistrés** (sync_log) : 2 92+- **Syncs enregistrés** (sync_log) : 4
93 93
94 ## Erreurs & dépannage 94 ## Erreurs & dépannage
95 95
added docs/connecteurs/cantonsdelest.md +105 −0
@@ -0,0 +1,105 @@
1+# Tourisme Cantons-de-l'Est — calendrier officiel (ATR Estrie) (`cantonsdelest`)
2+
3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4+
5+**Statut : actif** · Type : `tourisme-regional` · Connecteur : `sortika/connectors/cantonsdelest.py`
6+
7+## Description
8+
9+- **Source** : [Tourisme Cantons-de-l'Est — calendrier officiel (ATR Estrie)](https://www.cantonsdelest.com/evenements)
10+- **Couverture** : Festivals, spectacles, événements — Cantons-de-l'Est (Estrie)
11+- **Extraction** : page liste rendue serveur (~25 fiches à venir) puis fiches détail incrémentales (cache data/cantonsdelest_cache.json, 40 fiches/sync, re-visite 7 j) : JSON-LD Event complet (nom, dates, description riche, adresse civique, images Tourinsoft mto.media.tourinsoft.eu) + GPS exact (data-lat/data-lng de la carte) ; même plateforme Tourinsoft que le SIT Québec → recoupement géré par dedup_key
12+
13+## Accès
14+
15+- **Type** : tourisme-regional
16+- **Endpoint** (`BASE`) : `https://www.cantonsdelest.com`
17+- **Auth** : Aucune (accès public, identification honnête par User-Agent)
18+- **Pagination** : aucune (flux unique)
19+- **Backend** : GET direct `requests` (throttlé, UA `SortiKa/1.0` identifié)
20+- **Throttle** : 0.8 s entre requêtes
21+- **Accès légal** : pages publiques de l'ATR, rendu serveur — GET throttlés 0,8 s, identification honnête, lien vers la fiche originale
22+
23+<details><summary>En-tête documentaire du connecteur</summary>
24+
25+> connectors/cantonsdelest.py : connecteur Tourisme Cantons-de-l'Est (ATR Estrie)
26+> Source : cantonsdelest.com/evenements — calendrier officiel de
27+> l'association touristique régionale (données Tourinsoft, la
28+> même plateforme que le SIT Québec : médias mto.tourinsoft.eu).
29+> Extraction: page liste rendue serveur (~25 fiches à venir, liens
30+> /evenements/<id>/<slug>[/<idDate>]) puis fiches détail
31+> incrémentales (cache disque data/cantonsdelest_cache.json,
32+> plafond CDE_FICHE_MAX/sync, re-visite 7 j) : JSON-LD Event
33+> complet (nom, dates, description riche, adresse civique,
34+> images) + GPS exact (attributs data-lat/data-lng de la carte).
35+> Accès : pages publiques de l'ATR, rendu serveur — GET directs
36+> throttlés, identification honnête, lien vers la fiche
37+> originale. Recoupement avec le flux SITQ géré par dedup_key.
38+> Prix : non publié de façon structurée → jamais inventé.
39+
40+</details>
41+
42+## Champs récupérés → schéma
43+
44+Complétude mesurée sur les **13 événements actifs** de la source ; exemples tirés d'un événement réel (`cantonsdelest:329`).
45+
46+| Champ (`events.*`) | Description | Complétude | Exemple réel |
47+|---|---|---|---|
48+| `title` — Titre | titre de l'événement | 100 % | ShazamFest |
49+| `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Ce festival rend hommage à la créativité, la communauté et l'expression artistique unique, réuni… |
50+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://www.cantonsdelest.com/evenements/329/shazamfest |
51+| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["autre"] |
52+| `venue` — Salle (venue) | nom du lieu | 0 % | ∅ |
53+| `address` — Adresse | adresse civique | 85 % | 2722 Chemin de Ways Mills |
54+| `city` — Ville | municipalité | 100 % | Barnston-Ouest |
55+| `region` — Région | région administrative | 100 % | Estrie |
56+| `lat` — GPS (lat/lng) | coordonnées géographiques | 77 % | 45.126982, -71.99712 |
57+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-07-09 |
58+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-07-12 |
59+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 0 % | ∅ |
60+| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ |
61+| `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
62+| `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
63+| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
64+| `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
65+| `image` — Image | visuel (og:image ou média source) | 100 % | https://mto.media.tourinsoft.eu/upload/CD1-0618.jpg?width=1191&height=670 |
66+| `website` — Site web | site officiel de l'événement | 0 % | ∅ |
67+
68+## Fréquence & budgets
69+
70+- **Cadence** : horaire (watcher PM2, incrémental)
71+- **Budgets / plafonds (variables d'environnement)** :
72+
73+| Variable | Défaut | Rôle |
74+|---|---|---|
75+| `CDE_FICHE_MAX` | `40` | fiches/sync |
76+- **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque).
77+
78+## Volumétrie & complétude live
79+
80+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
81+
82+- **Événements en base** : 13 (dont **13 actifs**)
83+- **GPS** : 10/13 actifs (77 %)
84+- **Prix** : 0/13 actifs (0 %)
85+- **Heure (start_time)** : 0/13 actifs (0 %)
86+- **Artistes** : 0/13 actifs (0 %)
87+- **Image** : 13/13 actifs (100 %)
88+- **Salle (venue)** : 0/13 actifs (0 %)
89+- **Dernier sync** : 2026-08-18 05:52 — fetched 13, +13 ajoutés, ~0 mis à jour, -0 retirés
90+- **Syncs enregistrés** (sync_log) : 1
91+
92+## Erreurs & dépannage
93+
94+Aucune erreur dans `sync_log` pour cette source. ✅
95+
96+- Rejouer un sync isolé : `python3 run.py sync --source cantonsdelest` (ou via le watcher PM2) ; inspecter ensuite `sync_log` : `sqlite3 data/sortika.db "SELECT * FROM sync_log WHERE source='cantonsdelest' ORDER BY ts DESC LIMIT 5;"`
97+
98+## Licence / attribution
99+
100+- pages publiques de l'ATR, rendu serveur — GET throttlés 0,8 s, identification honnête, lien vers la fiche originale
101+
102+## Historique
103+
104+- **2026-08-18 — Vague 1 d'enrichissement** (commit `7d9d1b0`) : géocodage Nominatim des salles (`sortika/geocode.py`, cache disque, plafond `SORTIKA_GEOCODE_MAX`), fiches détail lepointdevente (description, heure, tarifs réels `/plugins/rates/<id>`), contacts Tourinsoft (SITQ), heure locale Montréal via JSON-LD.
105+- **2026-08-18 — Vague 2 d'enrichissement** (commit `65d2d33`) : colonnes additives `events.start_time` (HH:MM locale QC) et `events.artists` (JSON) + migration auto ; heure captée partout où la source la publie (lepointdevente, evenko, Laval, Sherbrooke, Montréal, atuvu) ; artistes (headliners evenko, lineup Bandsintown, attractions Ticketmaster, patrons sûrs sur les titres) ; 3 nouveaux connecteurs (brossard, ticketmaster, bandsintown).
modified docs/connecteurs/evenko.md +13 −13
@@ -1,6 +1,6 @@
1 # evenko — grands concerts et festivals (`evenko`) 1 # evenko — grands concerts et festivals (`evenko`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `promoteur` · Connecteur : `sortika/connectors/evenko.py` 5 **Statut : actif** · Type : `promoteur` · Connecteur : `sortika/connectors/evenko.py`
6 6
@@ -40,7 +40,7 @@
40 40
41 ## Champs récupérés → schéma 41 ## Champs récupérés → schéma
42 42
43−Complétude mesurée sur les **423 événements actifs** de la source ; exemples tirés d'un événement réel (`evenko:e008791`). 43+Complétude mesurée sur les **421 événements actifs** de la source ; exemples tirés d'un événement réel (`evenko:e008791`).
44 44
45 | Champ (`events.*`) | Description | Complétude | Exemple réel | 45 | Champ (`events.*`) | Description | Complétude | Exemple réel |
46 |---|---|---|---| 46 |---|---|---|---|
@@ -71,17 +71,17 @@ Complétude mesurée sur les **423 événements actifs** de la source ; exemples
71 71
72 ## Volumétrie & complétude live 72 ## Volumétrie & complétude live
73 73
74−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 74+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
75− 75+
76−- **Événements en base** : 423 (dont **423 actifs**) 76+- **Événements en base** : 423 (dont **421 actifs**)
77−- **GPS** : 327/423 actifs (77 %) 77+- **GPS** : 326/421 actifs (77 %)
78−- **Prix** : 0/423 actifs (0 %) 78+- **Prix** : 0/421 actifs (0 %)
79−- **Heure (start_time)** : 420/423 actifs (99 %) 79+- **Heure (start_time)** : 418/421 actifs (99 %)
80−- **Artistes** : 420/423 actifs (99 %) 80+- **Artistes** : 418/421 actifs (99 %)
81−- **Image** : 423/423 actifs (100 %) 81+- **Image** : 421/421 actifs (100 %)
82−- **Salle (venue)** : 423/423 actifs (100 %) 82+- **Salle (venue)** : 421/421 actifs (100 %)
83−- **Dernier sync** : 2026-08-18 02:35 — fetched 423, +0 ajoutés, ~9 mis à jour, -0 retirés 83+- **Dernier sync** : 2026-08-18 04:55 — fetched 421, +0 ajoutés, ~0 mis à jour, -2 retirés
84−- **Syncs enregistrés** (sync_log) : 18 84+- **Syncs enregistrés** (sync_log) : 20
85 85
86 ## Erreurs & dépannage 86 ## Erreurs & dépannage
87 87
added docs/connecteurs/eventbrite.md +110 −0
@@ -0,0 +1,110 @@
1+# Eventbrite — billetterie libre-service (Québec) (`eventbrite`)
2+
3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4+
5+**Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/eventbrite.py`
6+
7+## Description
8+
9+- **Source** : [Eventbrite — billetterie libre-service (Québec)](https://www.eventbrite.ca/d/canada--montreal/all-events/)
10+- **Couverture** : Événements communautaires, ateliers, festivals, affaires — grandes villes du Québec
11+- **Extraction** : pages de découverte publiques /d/<lieu>/all-events/?page=N (7 lieux QC pondérés : Montréal, Québec, Laval, Gatineau, Sherbrooke, Longueuil, Trois-Rivières) — window.__SERVER_DATA__ livre 20 résultats/page : nom, résumé, date + heure locales, salle géocodée (lat/lng, adresse, code postal), image, catégories (tags Eventbrite*), URL ; prix par fiche détail /e/<slug> incrémentale (JSON-LD AggregateOffer lowPrice/highPrice CAD ; cache data/eventbrite_cache.json, 150 fiches/sync, re-visite 7 j) ; en ligne exclus, filtre region=QC strict
12+
13+## Accès
14+
15+- **Type** : billetterie
16+- **Endpoint** (`LIST_URL`) : `https://www.eventbrite.ca/d/{place}/all-events/?page={page}`
17+- **Auth** : Aucune (accès public, identification honnête par User-Agent)
18+- **Pagination** : par numéro de page
19+- **Backend** : GET direct `requests` (throttlé, UA `SortiKa/1.0` identifié)
20+- **Throttle** : 0.8 s entre requêtes
21+- **Accès légal** : robots.txt « User-agent: * » n'interdit ni /d/ ni /e/ (seuls /directory/, checkout et endpoints privés) — GET throttlés 0,8 s, identification honnête, lien direct vers chaque fiche
22+
23+<details><summary>En-tête documentaire du connecteur</summary>
24+
25+> connectors/eventbrite.py : connecteur Eventbrite (Québec) — SANS clé
26+> Source : eventbrite.ca — billetterie libre-service (communautaire,
27+> ateliers, festivals, affaires, vie nocturne...).
28+> Extraction: pages de découverte publiques /d/<lieu>/all-events/?page=N —
29+> chaque page embarque window.__SERVER_DATA__ avec les résultats
30+> de recherche COMPLETS (20 événements/page) : nom, résumé,
31+> date + heure locales, salle géocodée (lat/lng, adresse, code
32+> postal), image, catégories (tags Eventbrite*), URL. Vérifié
33+> 2026-08-18 : Montréal 3 341 événements (49 pages exposées).
34+> Prix : fiches détail /e/<slug> incrémentales (JSON-LD
35+> AggregateOffer lowPrice/highPrice CAD) avec cache disque
36+> data/eventbrite_cache.json, plafond EB_DETAIL_MAX fiches/sync
37+> (défaut 150), re-visite 7 jours — patron La Vitrine.
38+> Accès : robots.txt « User-agent: * » n'interdit ni /d/ ni /e/ (seuls
39+> /directory/, checkout et endpoints privés le sont) — GET
40+> directs throttlés, identification honnête, lien direct vers
41+> chaque fiche Eventbrite. Filtre region=QC ; en ligne exclus.
42+> Prix : uniquement l'AggregateOffer publié par Eventbrite — jamais
43+> inventé (gratuit seulement si high == 0).
44+
45+</details>
46+
47+## Champs récupérés → schéma
48+
49+Complétude mesurée sur les **426 événements actifs** de la source ; exemples tirés d'un événement réel (`eventbrite:1989398621831`).
50+
51+| Champ (`events.*`) | Description | Complétude | Exemple réel |
52+|---|---|---|---|
53+| `title` — Titre | titre de l'événement | 100 % | SUGAR SAMMY - MONTRÉAL - EN FRANÇAIS |
54+| `description` — Description | texte descriptif (source ou fiche détail) | 100 % | VERSION 100% FRANCO DE YOU'RE GONNA RIRE 2 |
55+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://www.eventbrite.ca/e/billets-sugar-sammy-montreal-en-francais-1989398621831 |
56+| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
57+| `venue` — Salle (venue) | nom du lieu | 100 % | Salle Pierre-Mercure |
58+| `address` — Adresse | adresse civique | 100 % | 300 Boulevard de Maisonneuve Est |
59+| `city` — Ville | municipalité | 100 % | Montréal |
60+| `region` — Région | région administrative | 99 % | Montréal |
61+| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.513753, -73.562262 |
62+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-12 |
63+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-12 |
64+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 100 % | 19:00 |
65+| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 65 % | 0 |
66+| `price_min` — Prix min | tarif minimal réel ($) | 63 % | 59.99 |
67+| `price_label` — Libellé prix | étiquette tarifaire affichable | 65 % | 59.99 $ à 79.99 $ (Eventbrite) |
68+| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
69+| `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
70+| `image` — Image | visuel (og:image ou média source) | 97 % | https://img.evbuc.com/https%3A%2F%2Fcdn.evbuc.com%2Fimages%2F1184519371%2F309873035014%2F1%2Fori… |
71+| `website` — Site web | site officiel de l'événement | 0 % | ∅ |
72+
73+## Fréquence & budgets
74+
75+- **Cadence** : horaire (watcher PM2, incrémental)
76+- **Budgets / plafonds (variables d'environnement)** :
77+
78+| Variable | Défaut | Rôle |
79+|---|---|---|
80+| `EB_DETAIL_MAX` | `150` | fiches prix/sync |
81+- **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque).
82+
83+## Volumétrie & complétude live
84+
85+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
86+
87+- **Événements en base** : 426 (dont **426 actifs**)
88+- **GPS** : 426/426 actifs (100 %)
89+- **Prix** : 275/426 actifs (65 %)
90+- **Heure (start_time)** : 426/426 actifs (100 %)
91+- **Artistes** : 0/426 actifs (0 %)
92+- **Image** : 415/426 actifs (97 %)
93+- **Salle (venue)** : 426/426 actifs (100 %)
94+- **Dernier sync** : 2026-08-18 05:52 — fetched 426, +426 ajoutés, ~0 mis à jour, -0 retirés
95+- **Syncs enregistrés** (sync_log) : 1
96+
97+## Erreurs & dépannage
98+
99+Aucune erreur dans `sync_log` pour cette source. ✅
100+
101+- Rejouer un sync isolé : `python3 run.py sync --source eventbrite` (ou via le watcher PM2) ; inspecter ensuite `sync_log` : `sqlite3 data/sortika.db "SELECT * FROM sync_log WHERE source='eventbrite' ORDER BY ts DESC LIMIT 5;"`
102+
103+## Licence / attribution
104+
105+- robots.txt « User-agent: * » n'interdit ni /d/ ni /e/ (seuls /directory/, checkout et endpoints privés) — GET throttlés 0,8 s, identification honnête, lien direct vers chaque fiche
106+
107+## Historique
108+
109+- **2026-08-18 — Vague 1 d'enrichissement** (commit `7d9d1b0`) : géocodage Nominatim des salles (`sortika/geocode.py`, cache disque, plafond `SORTIKA_GEOCODE_MAX`), fiches détail lepointdevente (description, heure, tarifs réels `/plugins/rates/<id>`), contacts Tourinsoft (SITQ), heure locale Montréal via JSON-LD.
110+- **2026-08-18 — Vague 2 d'enrichissement** (commit `65d2d33`) : colonnes additives `events.start_time` (HH:MM locale QC) et `events.artists` (JSON) + migration auto ; heure captée partout où la source la publie (lepointdevente, evenko, Laval, Sherbrooke, Montréal, atuvu) ; artistes (headliners evenko, lineup Bandsintown, attractions Ticketmaster, patrons sûrs sur les titres) ; 3 nouveaux connecteurs (brossard, ticketmaster, bandsintown).
modified docs/connecteurs/gatineau.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Gatineau — Événements (`gatineau`) 1 # Ville de Gatineau — Événements (`gatineau`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
modified docs/connecteurs/laval.md +4 −4
@@ -1,6 +1,6 @@
1 # Ville de Laval — Calendrier des activités (`laval`) 1 # Ville de Laval — Calendrier des activités (`laval`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/laval.py` 5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/laval.py`
6 6
@@ -64,7 +64,7 @@ Complétude mesurée sur les **277 événements actifs** de la source ; exemples
64 64
65 ## Volumétrie & complétude live 65 ## Volumétrie & complétude live
66 66
67−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 67+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
68 68
69 - **Événements en base** : 277 (dont **277 actifs**) 69 - **Événements en base** : 277 (dont **277 actifs**)
70 - **GPS** : 142/277 actifs (51 %) 70 - **GPS** : 142/277 actifs (51 %)
@@ -73,8 +73,8 @@ Complétude mesurée sur les **277 événements actifs** de la source ; exemples
73 - **Artistes** : 0/277 actifs (0 %) 73 - **Artistes** : 0/277 actifs (0 %)
74 - **Image** : 271/277 actifs (98 %) 74 - **Image** : 271/277 actifs (98 %)
75 - **Salle (venue)** : 248/277 actifs (90 %) 75 - **Salle (venue)** : 248/277 actifs (90 %)
76−- **Dernier sync** : 2026-08-18 02:35 — fetched 293, +0 ajoutés, ~32 mis à jour, -0 retirés 76+- **Dernier sync** : 2026-08-18 04:55 — fetched 293, +0 ajoutés, ~32 mis à jour, -0 retirés
77−- **Syncs enregistrés** (sync_log) : 21 77+- **Syncs enregistrés** (sync_log) : 23
78 78
79 ## Erreurs & dépannage 79 ## Erreurs & dépannage
80 80
modified docs/connecteurs/lavitrine.md +4 −4
@@ -1,6 +1,6 @@
1 # La Vitrine culturelle (`lavitrine`) 1 # La Vitrine culturelle (`lavitrine`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/lavitrine.py` 5 **Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/lavitrine.py`
6 6
@@ -77,7 +77,7 @@ Complétude mesurée sur les **5127 événements actifs** de la source ; exemple
77 77
78 ## Volumétrie & complétude live 78 ## Volumétrie & complétude live
79 79
80−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 80+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
81 81
82 - **Événements en base** : 5137 (dont **5127 actifs**) 82 - **Événements en base** : 5137 (dont **5127 actifs**)
83 - **GPS** : 2681/5127 actifs (52 %) 83 - **GPS** : 2681/5127 actifs (52 %)
@@ -86,8 +86,8 @@ Complétude mesurée sur les **5127 événements actifs** de la source ; exemple
86 - **Artistes** : 0/5127 actifs (0 %) 86 - **Artistes** : 0/5127 actifs (0 %)
87 - **Image** : 5127/5127 actifs (100 %) 87 - **Image** : 5127/5127 actifs (100 %)
88 - **Salle (venue)** : 5125/5127 actifs (100 %) 88 - **Salle (venue)** : 5125/5127 actifs (100 %)
89−- **Dernier sync** : 2026-08-18 02:35 — fetched 5127, +0 ajoutés, ~5127 mis à jour, -0 retirés 89+- **Dernier sync** : 2026-08-18 04:55 — fetched 5127, +0 ajoutés, ~0 mis à jour, -0 retirés
90−- **Syncs enregistrés** (sync_log) : 11 90+- **Syncs enregistrés** (sync_log) : 13
91 91
92 ## Erreurs & dépannage 92 ## Erreurs & dépannage
93 93
modified docs/connecteurs/lepointdevente.md +18 −18
@@ -1,6 +1,6 @@
1 # Le point de vente — billetterie québécoise (`lepointdevente`) 1 # Le point de vente — billetterie québécoise (`lepointdevente`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/lepointdevente.py` 5 **Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/lepointdevente.py`
6 6
@@ -44,28 +44,28 @@
44 44
45 ## Champs récupérés → schéma 45 ## Champs récupérés → schéma
46 46
47−Complétude mesurée sur les **1787 événements actifs** de la source ; exemples tirés d'un événement réel (`lepointdevente:98a260908001`). 47+Complétude mesurée sur les **1787 événements actifs** de la source ; exemples tirés d'un événement réel (`lepointdevente:ghj260915003`).
48 48
49 | Champ (`events.*`) | Description | Complétude | Exemple réel | 49 | Champ (`events.*`) | Description | Complétude | Exemple réel |
50 |---|---|---|---| 50 |---|---|---|---|
51−| `title` — Titre | titre de l'événement | 100 % | GRANDE ALLÉE - Souper tardif festif | 51+| `title` — Titre | titre de l'événement | 100 % | FAUBOURG SAINT-JEAN - Vie de quartier |
52−| `description` — Description | texte descriptif (source ou fiche détail) | 31 % | Séance d'information des Missions commerciales de l'Université Laval!Le programme des Missions c… | 52+| `description` — Description | texte descriptif (source ou fiche détail) | 46 % | Un souper gourmand dans la haute-ville !Découvrez les restaurants chouchous des locaux dans ce q… |
53−| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://lepointdevente.com/billets/98a260908001 | 53+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://lepointdevente.com/billets/ghj260915003 |
54 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["autre"] | 54 | `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["autre"] |
55−| `venue` — Salle (venue) | nom du lieu | 99 % | Grande-Allée | 55+| `venue` — Salle (venue) | nom du lieu | 99 % | Faubourg St-Jean |
56 | `address` — Adresse | adresse civique | 0 % | ∅ | 56 | `address` — Adresse | adresse civique | 0 % | ∅ |
57 | `city` — Ville | municipalité | 100 % | Québec | 57 | `city` — Ville | municipalité | 100 % | Québec |
58 | `region` — Région | région administrative | 98 % | Capitale-Nationale | 58 | `region` — Région | région administrative | 98 % | Capitale-Nationale |
59−| `lat` — GPS (lat/lng) | coordonnées géographiques | 27 % | 46.2897843, -72.6724193 | 59+| `lat` — GPS (lat/lng) | coordonnées géographiques | 27 % | 45.2564309, -73.6230701 |
60−| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-08 | 60+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-15 |
61−| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-08 | 61+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-15 |
62−| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 94 % | 19:00 | 62+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 94 % | 17:00 |
63−| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 31 % | 1 | 63+| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 46 % | 0 |
64−| `price_min` — Prix min | tarif minimal réel ($) | 29 % | ∅ | 64+| `price_min` — Prix min | tarif minimal réel ($) | 45 % | 261.26 |
65−| `price_label` — Libellé prix | étiquette tarifaire affichable | 31 % | Admission générale 0 $ | 65+| `price_label` — Libellé prix | étiquette tarifaire affichable | 46 % | Admission générale 261.26 $ |
66 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 1 % | [] | 66 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 1 % | [] |
67 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ | 67 | `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
68−| `image` — Image | visuel (og:image ou média source) | 100 % | https://tpos.s3.amazonaws.com/events/98A/26/09/03/001/98a260903001-1152x648-fr-6a677853.png | 68+| `image` — Image | visuel (og:image ou média source) | 100 % | https://tpos.s3.amazonaws.com/events/GHJ/26/01/15/001/ghj260115001-1152x648-fr-695b81b1.png |
69 | `website` — Site web | site officiel de l'événement | 0 % | ∅ | 69 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
70 70
71 ## Fréquence & budgets 71 ## Fréquence & budgets
@@ -80,17 +80,17 @@ Complétude mesurée sur les **1787 événements actifs** de la source ; exemple
80 80
81 ## Volumétrie & complétude live 81 ## Volumétrie & complétude live
82 82
83−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 83+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
84 84
85 - **Événements en base** : 4191 (dont **1787 actifs**) 85 - **Événements en base** : 4191 (dont **1787 actifs**)
86 - **GPS** : 476/1787 actifs (27 %) 86 - **GPS** : 476/1787 actifs (27 %)
87−- **Prix** : 547/1787 actifs (31 %) 87+- **Prix** : 823/1787 actifs (46 %)
88 - **Heure (start_time)** : 1673/1787 actifs (94 %) 88 - **Heure (start_time)** : 1673/1787 actifs (94 %)
89 - **Artistes** : 16/1787 actifs (1 %) 89 - **Artistes** : 16/1787 actifs (1 %)
90 - **Image** : 1787/1787 actifs (100 %) 90 - **Image** : 1787/1787 actifs (100 %)
91 - **Salle (venue)** : 1777/1787 actifs (99 %) 91 - **Salle (venue)** : 1777/1787 actifs (99 %)
92−- **Dernier sync** : 2026-08-18 02:41 — fetched 1787, +0 ajoutés, ~164 mis à jour, -0 retirés 92+- **Dernier sync** : 2026-08-18 05:00 — fetched 1787, +0 ajoutés, ~153 mis à jour, -0 retirés
93−- **Syncs enregistrés** (sync_log) : 22 93+- **Syncs enregistrés** (sync_log) : 24
94 94
95 ## Erreurs & dépannage 95 ## Erreurs & dépannage
96 96
modified docs/connecteurs/levis.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Lévis — Calendrier (`levis`) 1 # Ville de Lévis — Calendrier (`levis`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
modified docs/connecteurs/longueuil.md +77 −15
@@ -1,45 +1,107 @@
1−# Ville de Longueuil — Événements (`longueuil`) 1+# Ville de Longueuil — Événements (nouveau portail) (`longueuil`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5−**Statut : abandonnee** · Type : `donnees-ouvertes` · Connecteur : *aucun (source sans connecteur runtime)* 5+**Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/longueuil.py`
6 6
7 ## Description 7 ## Description
8 8
9−- **Source** : [Ville de Longueuil — Événements](https://www.donneesquebec.ca/recherche/dataset/evenements) 9+- **Source** : [Ville de Longueuil — Événements (nouveau portail)](https://longueuil.quebec/fr/evenements)
10−- **Couverture** : Longueuil (Montérégie) 10+- **Couverture** : Événements municipaux, Longueuil (Montérégie)
11−- **Extraction** : CSV (Données Québec) 11+- **Extraction** : page liste rendue serveur (portail Nuxt) — cartes <article class="event__item"> : <time datetime> (date + heure locales, début/fin), titre, lien, extrait ; enrichissement par fiche incrémental (cache data/longueuil_cache.json, 30 fiches/sync, re-visite 30 j) : adresse civique, code postal, municipalité depuis l'état Nuxt (address_line1/postal_code/locality) ; GPS via le géocodeur central
12−- **Note** : Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17) 12+- **Note** : Remplace le jeu Données Québec « evenements » périmé (dernière date 2017-03, vérifié 2026-08-17) par le portail vivant longueuil.quebec.
13 13
14 ## Accès 14 ## Accès
15 15
16−- **Type** : donnees-ouvertes 16+- **Type** : municipal
17−- **Endpoint** : voir URL source (https://www.donneesquebec.ca/recherche/dataset/evenements) 17+- **Endpoint** (`BASE`) : `https://longueuil.quebec`
18 - **Auth** : Aucune (accès public, identification honnête par User-Agent) 18 - **Auth** : Aucune (accès public, identification honnête par User-Agent)
19−- **Accès légal** : Licence CC 4.0 (Données Québec) 19+- **Pagination** : aucune (flux unique)
20+- **Backend** : GET direct `requests` (throttlé, UA `SortiKa/1.0` identifié)
21+- **Throttle** : 0.8 s entre requêtes
22+- **Accès légal** : portail municipal public, rendu serveur — GET throttlés 0,8 s, identification honnête
23+
24+<details><summary>En-tête documentaire du connecteur</summary>
25+
26+> connectors/longueuil.py : connecteur Ville de Longueuil — nouveau portail
27+> Source : longueuil.quebec/fr/evenements (portail Nuxt.js, rendu serveur).
28+> Extraction: cartes <article class="event__item"> de la page liste —
29+> <time datetime="2026-08-29T10:00:00-04:00"> (date + heure
30+> locales, parfois début ET fin), titre + lien, extrait.
31+> Enrichissement par fiche (incrémental, cache disque
32+> data/longueuil_cache.json, plafond LGL_FICHE_MAX/sync,
33+> re-visite 30 j) : adresse civique, code postal, municipalité
34+> extraits de l'état Nuxt (address_line1/postal_code/locality).
35+> GPS absent de la source → géocodeur central (salle + ville).
36+> Accès : portail municipal public, rendu serveur — GET directs
37+> throttlés, identification honnête. (L'ancien jeu « événements »
38+> sur Données Québec est périmé depuis 2017 — vérifié 2026-08-17 ;
39+> ce portail est la source vivante.)
40+> Prix : non publié dans la liste → jamais inventé.
41+
42+</details>
20 43
21 ## Champs récupérés → schéma 44 ## Champs récupérés → schéma
22 45
23−*Aucun événement actif en base pour cette source — pas de schéma live à montrer.* 46+Complétude mesurée sur les **9 événements actifs** de la source ; exemples tirés d'un événement réel (`longueuil:journee-de-la-biodiversite-au-parc-michel-chartrand`).
47+
48+| Champ (`events.*`) | Description | Complétude | Exemple réel |
49+|---|---|---|---|
50+| `title` — Titre | titre de l'événement | 100 % | Journée de la biodiversité au parc Michel-Chartrand |
51+| `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Découvrez le parc-nature Michel-Chartrand sous un nouvel angle lors de la première édition de la… |
52+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://longueuil.quebec/fr/evenements/journee-de-la-biodiversite-au-parc-michel-chartrand |
53+| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["plein-air"] |
54+| `venue` — Salle (venue) | nom du lieu | 0 % | ∅ |
55+| `address` — Adresse | adresse civique | 100 % | 1895, rue Adoncour |
56+| `city` — Ville | municipalité | 100 % | Longueuil |
57+| `region` — Région | région administrative | 100 % | Montérégie |
58+| `lat` — GPS (lat/lng) | coordonnées géographiques | 0 % | ∅ |
59+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-29 |
60+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-29 |
61+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 100 % | 10:00 |
62+| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ |
63+| `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
64+| `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
65+| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
66+| `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
67+| `image` — Image | visuel (og:image ou média source) | 0 % | ∅ |
68+| `website` — Site web | site officiel de l'événement | 0 % | ∅ |
24 69
25 ## Fréquence & budgets 70 ## Fréquence & budgets
26 71
27−- **Cadence** : n/d 72+- **Cadence** : horaire (watcher PM2, incrémental)
73+- **Budgets / plafonds (variables d'environnement)** :
74+
75+| Variable | Défaut | Rôle |
76+|---|---|---|
77+| `LGL_FICHE_MAX` | `30` | fiches/sync |
28 - **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque). 78 - **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque).
29 79
30 ## Volumétrie & complétude live 80 ## Volumétrie & complétude live
31 81
32−*Source sans connecteur runtime : aucune donnée en base.* 82+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
83+
84+- **Événements en base** : 9 (dont **9 actifs**)
85+- **GPS** : 0/9 actifs (0 %)
86+- **Prix** : 0/9 actifs (0 %)
87+- **Heure (start_time)** : 9/9 actifs (100 %)
88+- **Artistes** : 0/9 actifs (0 %)
89+- **Image** : 0/9 actifs (0 %)
90+- **Salle (venue)** : 0/9 actifs (0 %)
91+- **Dernier sync** : 2026-08-18 05:52 — fetched 9, +9 ajoutés, ~0 mis à jour, -0 retirés
92+- **Syncs enregistrés** (sync_log) : 1
33 93
34 ## Erreurs & dépannage 94 ## Erreurs & dépannage
35 95
96+Aucune erreur dans `sync_log` pour cette source. ✅
97+
98+- Rejouer un sync isolé : `python3 run.py sync --source longueuil` (ou via le watcher PM2) ; inspecter ensuite `sync_log` : `sqlite3 data/sortika.db "SELECT * FROM sync_log WHERE source='longueuil' ORDER BY ts DESC LIMIT 5;"`
36 99
37 ## Licence / attribution 100 ## Licence / attribution
38 101
39−- Licence CC 4.0 (Données Québec) 102+- portail municipal public, rendu serveur — GET throttlés 0,8 s, identification honnête
40 103
41 ## Historique 104 ## Historique
42 105
43−- **Source abandonnée** — Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17)
44 - **2026-08-18 — Vague 1 d'enrichissement** (commit `7d9d1b0`) : géocodage Nominatim des salles (`sortika/geocode.py`, cache disque, plafond `SORTIKA_GEOCODE_MAX`), fiches détail lepointdevente (description, heure, tarifs réels `/plugins/rates/<id>`), contacts Tourinsoft (SITQ), heure locale Montréal via JSON-LD. 106 - **2026-08-18 — Vague 1 d'enrichissement** (commit `7d9d1b0`) : géocodage Nominatim des salles (`sortika/geocode.py`, cache disque, plafond `SORTIKA_GEOCODE_MAX`), fiches détail lepointdevente (description, heure, tarifs réels `/plugins/rates/<id>`), contacts Tourinsoft (SITQ), heure locale Montréal via JSON-LD.
45 - **2026-08-18 — Vague 2 d'enrichissement** (commit `65d2d33`) : colonnes additives `events.start_time` (HH:MM locale QC) et `events.artists` (JSON) + migration auto ; heure captée partout où la source la publie (lepointdevente, evenko, Laval, Sherbrooke, Montréal, atuvu) ; artistes (headliners evenko, lineup Bandsintown, attractions Ticketmaster, patrons sûrs sur les titres) ; 3 nouveaux connecteurs (brossard, ticketmaster, bandsintown). 107 - **2026-08-18 — Vague 2 d'enrichissement** (commit `65d2d33`) : colonnes additives `events.start_time` (HH:MM locale QC) et `events.artists` (JSON) + migration auto ; heure captée partout où la source la publie (lepointdevente, evenko, Laval, Sherbrooke, Montréal, atuvu) ; artistes (headliners evenko, lineup Bandsintown, attractions Ticketmaster, patrons sûrs sur les titres) ; 3 nouveaux connecteurs (brossard, ticketmaster, bandsintown).
modified docs/connecteurs/montreal.md +17 −17
@@ -1,6 +1,6 @@
1 # Ville de Montréal — Événements publics (`montreal`) 1 # Ville de Montréal — Événements publics (`montreal`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/montreal.py` 5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/montreal.py`
6 6
@@ -37,25 +37,25 @@
37 37
38 ## Champs récupérés → schéma 38 ## Champs récupérés → schéma
39 39
40−Complétude mesurée sur les **5679 événements actifs** de la source ; exemples tirés d'un événement réel (`montreal:les-legendaires-cine-ete-111611`). 40+Complétude mesurée sur les **5679 événements actifs** de la source ; exemples tirés d'un événement réel (`montreal:swing-tonique-jazz-band-114189`).
41 41
42 | Champ (`events.*`) | Description | Complétude | Exemple réel | 42 | Champ (`events.*`) | Description | Complétude | Exemple réel |
43 |---|---|---|---| 43 |---|---|---|---|
44−| `title` — Titre | titre de l'événement | 100 % | Les Légendaires – Ciné-été | 44+| `title` — Titre | titre de l'événement | 100 % | Swing Tonique Jazz Band |
45−| `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Découvrez cette adaptation cinématographique des bandes dessinées de Patrick Sobral, réalisée pa… | 45+| `description` — Description | texte descriptif (source ou fiche détail) | 100 % | Fanfare Swing Tonique Jazz Band sur la Plaza St-Hubert le 20 août à 17h30. |
46−| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://montreal.ca/evenements/les-legendaires-cine-ete-111611 | 46+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://montreal.ca/evenements/swing-tonique-jazz-band-114189 |
47−| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["cinema"] | 47+| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
48−| `venue` — Salle (venue) | nom du lieu | 4 % | Cinéma Beaubien | 48+| `venue` — Salle (venue) | nom du lieu | 4 % | Plaza St-Hubert |
49−| `address` — Adresse | adresse civique | 4 % | 2396 Rue Beaubien E | 49+| `address` — Adresse | adresse civique | 4 % | Coin rue Bellechasse et rue St-Hubert |
50 | `city` — Ville | municipalité | 100 % | Montréal | 50 | `city` — Ville | municipalité | 100 % | Montréal |
51 | `region` — Région | région administrative | 100 % | Montréal | 51 | `region` — Région | région administrative | 100 % | Montréal |
52−| `lat` — GPS (lat/lng) | coordonnées géographiques | 97 % | 45.548115, -73.5921 | 52+| `lat` — GPS (lat/lng) | coordonnées géographiques | 97 % | 45.534722, -73.599719 |
53−| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-22 | 53+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-20 |
54−| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-22 | 54+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-20 |
55−| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 10 % | 09:30 | 55+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 21 % | 17:30 |
56 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 100 % | 1 | 56 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 100 % | 1 |
57 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ | 57 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
58−| `price_label` — Libellé prix | étiquette tarifaire affichable | 100 % | Gratuit — Avec billet | 58+| `price_label` — Libellé prix | étiquette tarifaire affichable | 100 % | Gratuit — Entrée libre |
59 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] | 59 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
60 | `organizer` — Organisateur | promoteur / organisme | 100 % | Ville de Montréal — Rosemont–La Petite-Patrie | 60 | `organizer` — Organisateur | promoteur / organisme | 100 % | Ville de Montréal — Rosemont–La Petite-Patrie |
61 | `image` — Image | visuel (og:image ou média source) | 58 % | https://res.cloudinary.com/villemontreal/image/upload/w_1200,h_630,c_fill/f_auto,dpr_auto,q_auto… | 61 | `image` — Image | visuel (og:image ou média source) | 58 % | https://res.cloudinary.com/villemontreal/image/upload/w_1200,h_630,c_fill/f_auto,dpr_auto,q_auto… |
@@ -73,17 +73,17 @@ Complétude mesurée sur les **5679 événements actifs** de la source ; exemple
73 73
74 ## Volumétrie & complétude live 74 ## Volumétrie & complétude live
75 75
76−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 76+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
77 77
78 - **Événements en base** : 5729 (dont **5679 actifs**) 78 - **Événements en base** : 5729 (dont **5679 actifs**)
79 - **GPS** : 5530/5679 actifs (97 %) 79 - **GPS** : 5530/5679 actifs (97 %)
80 - **Prix** : 5679/5679 actifs (100 %) 80 - **Prix** : 5679/5679 actifs (100 %)
81−- **Heure (start_time)** : 588/5679 actifs (10 %) 81+- **Heure (start_time)** : 1187/5679 actifs (21 %)
82 - **Artistes** : 0/5679 actifs (0 %) 82 - **Artistes** : 0/5679 actifs (0 %)
83 - **Image** : 3296/5679 actifs (58 %) 83 - **Image** : 3296/5679 actifs (58 %)
84 - **Salle (venue)** : 246/5679 actifs (4 %) 84 - **Salle (venue)** : 246/5679 actifs (4 %)
85−- **Dernier sync** : 2026-08-18 02:41 — fetched 5679, +0 ajoutés, ~300 mis à jour, -0 retirés 85+- **Dernier sync** : 2026-08-18 05:01 — fetched 5679, +0 ajoutés, ~299 mis à jour, -0 retirés
86−- **Syncs enregistrés** (sync_log) : 22 86+- **Syncs enregistrés** (sync_log) : 24
87 87
88 ## Erreurs & dépannage 88 ## Erreurs & dépannage
89 89
modified docs/connecteurs/repentigny.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Repentigny — Événements (`repentigny`) 1 # Ville de Repentigny — Événements (`repentigny`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `donnees-ouvertes` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `donnees-ouvertes` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
modified docs/connecteurs/saguenay.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Saguenay — Événements culturels (`saguenay`) 1 # Ville de Saguenay — Événements culturels (`saguenay`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `donnees-ouvertes` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `donnees-ouvertes` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
modified docs/connecteurs/sherbrooke.md +4 −4
@@ -1,6 +1,6 @@
1 # Ville de Sherbrooke — Calendrier des événements (`sherbrooke`) 1 # Ville de Sherbrooke — Calendrier des événements (`sherbrooke`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/sherbrooke.py` 5 **Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/sherbrooke.py`
6 6
@@ -67,7 +67,7 @@ Complétude mesurée sur les **271 événements actifs** de la source ; exemples
67 67
68 ## Volumétrie & complétude live 68 ## Volumétrie & complétude live
69 69
70−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 70+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
71 71
72 - **Événements en base** : 271 (dont **271 actifs**) 72 - **Événements en base** : 271 (dont **271 actifs**)
73 - **GPS** : 0/271 actifs (0 %) 73 - **GPS** : 0/271 actifs (0 %)
@@ -76,8 +76,8 @@ Complétude mesurée sur les **271 événements actifs** de la source ; exemples
76 - **Artistes** : 0/271 actifs (0 %) 76 - **Artistes** : 0/271 actifs (0 %)
77 - **Image** : 270/271 actifs (100 %) 77 - **Image** : 270/271 actifs (100 %)
78 - **Salle (venue)** : 0/271 actifs (0 %) 78 - **Salle (venue)** : 0/271 actifs (0 %)
79−- **Dernier sync** : 2026-08-18 02:41 — fetched 271, +0 ajoutés, ~0 mis à jour, -0 retirés 79+- **Dernier sync** : 2026-08-18 05:01 — fetched 271, +0 ajoutés, ~0 mis à jour, -0 retirés
80−- **Syncs enregistrés** (sync_log) : 14 80+- **Syncs enregistrés** (sync_log) : 16
81 81
82 ## Erreurs & dépannage 82 ## Erreurs & dépannage
83 83
modified docs/connecteurs/sitq.md +4 −4
@@ -1,6 +1,6 @@
1 # SIT Québec — Ministère du Tourisme (`sitq`) 1 # SIT Québec — Ministère du Tourisme (`sitq`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/sitq.py` 5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/sitq.py`
6 6
@@ -77,7 +77,7 @@ Complétude mesurée sur les **1598 événements actifs** de la source ; exemple
77 77
78 ## Volumétrie & complétude live 78 ## Volumétrie & complétude live
79 79
80−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 80+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
81 81
82 - **Événements en base** : 1606 (dont **1598 actifs**) 82 - **Événements en base** : 1606 (dont **1598 actifs**)
83 - **GPS** : 1548/1598 actifs (97 %) 83 - **GPS** : 1548/1598 actifs (97 %)
@@ -86,8 +86,8 @@ Complétude mesurée sur les **1598 événements actifs** de la source ; exemple
86 - **Artistes** : 0/1598 actifs (0 %) 86 - **Artistes** : 0/1598 actifs (0 %)
87 - **Image** : 558/1598 actifs (35 %) 87 - **Image** : 558/1598 actifs (35 %)
88 - **Salle (venue)** : 1050/1598 actifs (66 %) 88 - **Salle (venue)** : 1050/1598 actifs (66 %)
89−- **Dernier sync** : 2026-08-18 02:41 — fetched 1598, +0 ajoutés, ~1598 mis à jour, -0 retirés 89+- **Dernier sync** : 2026-08-18 05:01 — fetched 1598, +0 ajoutés, ~0 mis à jour, -0 retirés
90−- **Syncs enregistrés** (sync_log) : 21 90+- **Syncs enregistrés** (sync_log) : 23
91 91
92 ## Erreurs & dépannage 92 ## Erreurs & dépannage
93 93
modified docs/connecteurs/terrebonne.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Terrebonne — Calendrier (`terrebonne`) 1 # Ville de Terrebonne — Calendrier (`terrebonne`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
modified docs/connecteurs/ticketmaster.md +64 −35
@@ -1,72 +1,101 @@
1 # Ticketmaster — Discovery API (Québec) (`ticketmaster`) 1 # Ticketmaster — Discovery API (Québec) (`ticketmaster`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5−**Statut : prêt — clé requise** · Type : `billetterie` · Connecteur : `sortika/connectors/ticketmaster.py` 5+**Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/ticketmaster.py`
6 6
7 ## Description 7 ## Description
8 8
9 - **Source** : [Ticketmaster — Discovery API (Québec)](https://developer.ticketmaster.com/products-and-docs/apis/discovery-api/v2/) 9 - **Source** : [Ticketmaster — Discovery API (Québec)](https://developer.ticketmaster.com/products-and-docs/apis/discovery-api/v2/)
10−- **Couverture** : Grands spectacles, sports et concerts — partout au Québec 10+- **Couverture** : Grands spectacles, sports et concerts — Montréal/Laval/Brossard, Québec, Gatineau
11−- **Extraction** : API officielle Discovery v2 — GET JSON paginé (countryCode=CA & stateCode=QC, size=200, tri par date) : nom, dates + heure locale, salle géolocalisée (venues lat/lng), fourchette de prix (priceRanges min/max), images 16:9, classifications → catégories, attractions → artistes, URL de billetterie officielle ; throttle 0,25 s (quota 5 req/s) 11+- **Extraction** : SANS clé (mode par défaut) : scraping léger des pages découverte publiques www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city, gatineau) — l'état Next.js (__NEXT_DATA__, requête serveur cityEvents) livre 20 événements/page : titre, date UTC + fuseau → date/heure locales, salle géolocalisée (lat/lng), adresse, code postal, ville, URL billetterie, catégorie majeure, plan de salle (image) ; plafonds TM_MAX_EVENTS=360 / TM_PAGES_PER_CITY=8 ; Scrapfly ASP sans render_js en secours anti-bot ; aucun prix publié dans ce flux → jamais inventé. REPLI AUTOMATIQUE : si TICKETMASTER_API_KEY apparaît dans .env, bascule sur la Discovery API v2 officielle (priceRanges, attractions → artistes, images 16:9) — identifiants identiques, zéro doublon.
12−- **Note** : Nécessite TICKETMASTER_API_KEY dans .env (clé gratuite). Sans clé, le connecteur s'annonce « clé manquante » et ne retourne rien. 12+- **Note** : Fonctionne sans clé (scraping découverte). TICKETMASTER_API_KEY optionnelle : active le mode API riche (prix, artistes, images).
13 13
14 ## Accès 14 ## Accès
15 15
16 - **Type** : billetterie 16 - **Type** : billetterie
17 - **Endpoint** (`API`) : `https://app.ticketmaster.com/discovery/v2/events.json` 17 - **Endpoint** (`API`) : `https://app.ticketmaster.com/discovery/v2/events.json`
18+- **Endpoint** (`DISCOVER`) : `https://www.ticketmaster.ca/discover/{city}?page={page}`
18 - **Auth** : Clé API gratuite `TICKETMASTER_API_KEY` (developer.ticketmaster.com) 19 - **Auth** : Clé API gratuite `TICKETMASTER_API_KEY` (developer.ticketmaster.com)
19 - **Pagination** : par numéro de page 20 - **Pagination** : par numéro de page
20−- **Backend** : GET direct `requests` (throttlé, UA `SortiKa/1.0` identifié) 21+- **Backend** : Scrapfly (anti-bot / rendu JS) via `base.get_scrapfly()`
21−- **Throttle** : 0.25 s entre requêtes 22+- **Throttle** : 1.0 s entre requêtes
22−- **Accès légal** : API officielle publique avec clé gratuite (developer.ticketmaster.com) — conditions Ticketmaster Developer respectées 23+- **Accès légal** : pages découverte publiques rendues serveur — robots.txt n'interdit pas /discover/ (seuls /api/ismds/*) ; GET throttlés 1 s, lien direct vers la billetterie officielle ; API officielle (clé gratuite) utilisée dès qu'une clé est fournie
23 24
24 <details><summary>En-tête documentaire du connecteur</summary> 25 <details><summary>En-tête documentaire du connecteur</summary>
25 26
26−> connectors/ticketmaster.py : connecteur Ticketmaster Discovery API (Québec) 27+> connectors/ticketmaster.py : connecteur Ticketmaster Canada (Québec) — SANS clé
27−> Source : app.ticketmaster.com/discovery/v2/events — API officielle 28+> Mode principal (sans clé) : scraping léger des pages découverte publiques
28−> publique (clé gratuite sur developer.ticketmaster.com). 29+> www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city,
29−> Extraction: GET JSON paginé (countryCode=CA & stateCode=QC, size=200) — 30+> gatineau). Chaque page embarque son état Next.js (__NEXT_DATA__) : la
30−> nom, dates + heure locale, salle géolocalisée (venues lat/lng), 31+> requête serveur `cityEvents` y est déjà résolue avec 20 événements/page
31−> fourchette de prix (priceRanges min/max), images 16:9, 32+> (titre, date UTC + fuseau, salle géolocalisée lat/lng, adresse, code
32−> classifications (segment/genre) → catégories, attractions → 33+> postal, ville, URL billetterie, catégorie majeure, seatmap). Pagination
33−> artistes, URL de billetterie officielle. 34+> par ?page=N (vérifié 2026-08-18 : montreal total=1165, quebec-city=136).
34−> Accès : API officielle avec clé — quota 5 req/s & 5 000 req/jour 35+> Aucun prix publié dans ce flux → is_free/price_* inconnus (jamais
35−> (throttle 0,25 s ; ~10 requêtes/sync, pagination plafonnée par 36+> inventés). Artistes : patrons sûrs des titres (artists_from_title).
36−> l'API à 1 000 éléments par requête profonde). 37+> robots.txt : /discover/ non interdit (seuls /api/ismds/* le sont).
37−> Clé : TICKETMASTER_API_KEY dans .env (voir .env.example). Sans clé, 38+> Anti-bot : GET direct d'abord ; si bloqué → Scrapfly ASP SANS render_js.
38−> le connecteur s'annonce « clé manquante » et ne retourne rien — 39+> Plafonds : TM_MAX_EVENTS (défaut 360), TM_PAGES_PER_CITY (défaut 8).
39−> statut « prêt — clé requise » dans data/sources.json. 40+> Mode repli automatique (si TICKETMASTER_API_KEY apparaît dans .env) :
41+> Discovery API officielle v2 — plus riche (priceRanges, attractions →
42+> artistes, images 16:9). Les identifiants d'événements sont IDENTIQUES
43+> dans les deux modes → aucun doublon lors d'un changement de mode.
40 44
41 </details> 45 </details>
42 46
43 ## Champs récupérés → schéma 47 ## Champs récupérés → schéma
44 48
45−*Aucun événement actif en base pour cette source — pas de schéma live à montrer.* 49+Complétude mesurée sur les **330 événements actifs** de la source ; exemples tirés d'un événement réel (`ticketmaster:310064A20EB06FDE`).
50+
51+| Champ (`events.*`) | Description | Complétude | Exemple réel |
52+|---|---|---|---|
53+| `title` — Titre | titre de l'événement | 100 % | Mardi Latin Groove |
54+| `description` — Description | texte descriptif (source ou fiche détail) | 0 % | ∅ |
55+| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://www.ticketmaster.ca/mardi-latin-groove-montreal-quebec-08-18-2026/event/310064A20EB06FDE |
56+| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
57+| `venue` — Salle (venue) | nom du lieu | 100 % | Le Balcon X Terrasse |
58+| `address` — Adresse | adresse civique | 100 % | 463 Rue Sainte-Catherine Ouest |
59+| `city` — Ville | municipalité | 100 % | Montreal |
60+| `region` — Région | région administrative | 100 % | Montréal |
61+| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.505438, -73.568812 |
62+| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-18 |
63+| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-18 |
64+| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 96 % | 19:00 |
65+| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ |
66+| `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
67+| `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
68+| `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
69+| `organizer` — Organisateur | promoteur / organisme | 0 % | ∅ |
70+| `image` — Image | visuel (og:image ou média source) | 90 % | https://mapsapi.tmol.io/maps/geometry/3/event/310064A20EB06FDE/staticImage?type=png&systemId=HOS… |
71+| `website` — Site web | site officiel de l'événement | 0 % | ∅ |
46 72
47 ## Fréquence & budgets 73 ## Fréquence & budgets
48 74
49−- **Cadence** : horaire (watcher PM2) dès que la clé est fournie 75+- **Cadence** : horaire (watcher PM2)
50 - **Budgets / plafonds (variables d'environnement)** : 76 - **Budgets / plafonds (variables d'environnement)** :
51 77
52 | Variable | Défaut | Rôle | 78 | Variable | Défaut | Rôle |
53 |---|---|---| 79 |---|---|---|
80+| `TM_MAX_EVENTS` | `360` | — |
81+| `TM_PAGES_PER_CITY` | `8` | — |
82+| `SCRAPFLY_API_KEY` | `—` | — |
54 | `TICKETMASTER_API_KEY` | `` | — | 83 | `TICKETMASTER_API_KEY` | `` | — |
55 - **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque). 84 - **Budget global** : géocodage Nominatim plafonné à `SORTIKA_GEOCODE_MAX` (défaut 120) requêtes réseau/cycle, toutes sources confondues (`sortika/geocode.py`, cache disque).
56 85
57 ## Volumétrie & complétude live 86 ## Volumétrie & complétude live
58 87
59−*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)* 88+*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
60 89
61−- **Événements en base** : 0 (dont **0 actifs**) 90+- **Événements en base** : 330 (dont **330 actifs**)
62−- **GPS** : 0/0 actifs (—) 91+- **GPS** : 330/330 actifs (100 %)
63−- **Prix** : 0/0 actifs (—) 92+- **Prix** : 0/330 actifs (0 %)
64−- **Heure (start_time)** : 0/0 actifs (—) 93+- **Heure (start_time)** : 318/330 actifs (96 %)
65−- **Artistes** : 0/0 actifs (—) 94+- **Artistes** : 0/330 actifs (0 %)
66−- **Image** : 0/0 actifs (—) 95+- **Image** : 297/330 actifs (90 %)
67−- **Salle (venue)** : 0/0 actifs (—) 96+- **Salle (venue)** : 330/330 actifs (100 %)
68−- **Dernier sync** : 2026-08-18 02:41 — fetched 0, +0 ajoutés, ~0 mis à jour, -0 retirés 97+- **Dernier sync** : 2026-08-18 05:51 — fetched 330, +330 ajoutés, ~0 mis à jour, -0 retirés
69−- **Syncs enregistrés** (sync_log) : 2 98+- **Syncs enregistrés** (sync_log) : 5
70 99
71 ## Erreurs & dépannage 100 ## Erreurs & dépannage
72 101
@@ -77,7 +106,7 @@ Aucune erreur dans `sync_log` pour cette source. ✅
77 106
78 ## Licence / attribution 107 ## Licence / attribution
79 108
80−- API officielle publique avec clé gratuite (developer.ticketmaster.com) — conditions Ticketmaster Developer respectées 109+- pages découverte publiques rendues serveur — robots.txt n'interdit pas /discover/ (seuls /api/ismds/*) ; GET throttlés 1 s, lien direct vers la billetterie officielle ; API officielle (clé gratuite) utilisée dès qu'une clé est fournie
81 110
82 ## Historique 111 ## Historique
83 112
modified docs/connecteurs/trois-rivieres.md +1 −1
@@ -1,6 +1,6 @@
1 # Ville de Trois-Rivières — Événements (`trois-rivieres`) 1 # Ville de Trois-Rivières — Événements (`trois-rivieres`)
2 2
3−> Fiche générée le 2026-08-18 03:22 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script. 3+> Fiche générée le 2026-08-18 05:52 EDT par `scripts/gen_connector_docs.py` — ne pas éditer à la main, relancer le script.
4 4
5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)* 5 **Statut : abandonnee** · Type : `municipal` · Connecteur : *aucun (source sans connecteur runtime)*
6 6
added sortika/connectors/cantonsdelest.py +135 −0
@@ -0,0 +1,135 @@
1+# -----------------------------------------------------------------------------
2+# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3+# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4+# connectors/cantonsdelest.py : connecteur Tourisme Cantons-de-l'Est (ATR Estrie)
5+# Source : cantonsdelest.com/evenements — calendrier officiel de
6+# l'association touristique régionale (données Tourinsoft, la
7+# même plateforme que le SIT Québec : médias mto.tourinsoft.eu).
8+# Extraction: page liste rendue serveur (~25 fiches à venir, liens
9+# /evenements/<id>/<slug>[/<idDate>]) puis fiches détail
10+# incrémentales (cache disque data/cantonsdelest_cache.json,
11+# plafond CDE_FICHE_MAX/sync, re-visite 7 j) : JSON-LD Event
12+# complet (nom, dates, description riche, adresse civique,
13+# images) + GPS exact (attributs data-lat/data-lng de la carte).
14+# Accès : pages publiques de l'ATR, rendu serveur — GET directs
15+# throttlés, identification honnête, lien vers la fiche
16+# originale. Recoupement avec le flux SITQ géré par dedup_key.
17+# Prix : non publié de façon structurée → jamais inventé.
18+# -----------------------------------------------------------------------------
19+from __future__ import annotations
20+
21+import json
22+import os
23+import re
24+import time
25+
26+from ..normalize import clean_text
27+from ..schema import Event
28+from .base import BaseConnector
29+
30+BASE = "https://www.cantonsdelest.com"
31+LIST_URL = BASE + "/evenements"
32+CDE_FICHE_MAX = int(os.environ.get("CDE_FICHE_MAX", "40")) # fiches/sync
33+REFRESH_AFTER = 7 * 86400
34+
35+_LINK_RE = re.compile(r"href=\"(/evenements/\d+/[^\"#?]+)\"")
36+_LD_JSON_RE = re.compile(r"application/ld\+json[^>]*>")
37+_GPS_RE = re.compile(r"data-lat=\"(-?\d+\.?\d*)\" data-lng=\"(-?\d+\.?\d*)\"")
38+
39+
40+def _ld_event(html: str) -> dict | None:
41+ """Premier bloc JSON-LD de type Event d'une fiche."""
42+ for m in _LD_JSON_RE.finditer(html):
43+ try:
44+ d, _ = json.JSONDecoder().raw_decode(html[m.end():].lstrip())
45+ except Exception:
46+ continue
47+ for doc in (d if isinstance(d, list) else [d]):
48+ if isinstance(doc, dict) and doc.get("@type") == "Event":
49+ return doc
50+ return None
51+
52+
53+class CantonsDeLEstConnector(BaseConnector):
54+ source_id = "cantonsdelest"
55+ request_delay = 0.8
56+
57+ def _parse_fiche(self, path: str, html: str) -> dict | None:
58+ ld = _ld_event(html)
59+ if not ld or not ld.get("name"):
60+ return None
61+ addr = ((ld.get("location") or {}).get("address") or {})
62+ gps = _GPS_RE.search(html)
63+ images = ld.get("image") or []
64+ if isinstance(images, str):
65+ images = [images]
66+ parts = path.strip("/").split("/") # evenements/<id>/<slug>[/<idDate>]
67+ ext = parts[1] + (f"-{parts[3]}" if len(parts) > 3 else "")
68+ return {
69+ "external_id": ext,
70+ "url": BASE + path,
71+ "title": clean_text(ld.get("name") or ""),
72+ "description": clean_text(ld.get("description") or ""),
73+ "raw_categories": [clean_text(ld.get("name") or ""),
74+ parts[2].replace("-", " ")],
75+ "address": clean_text(addr.get("streetAddress") or ""),
76+ "city": clean_text(addr.get("addressLocality") or ""),
77+ "postal_code": (addr.get("postalCode") or "").strip(),
78+ "lat": float(gps.group(1)) if gps else None,
79+ "lng": float(gps.group(2)) if gps else None,
80+ "start_date": ld.get("startDate"),
81+ "end_date": ld.get("endDate") or ld.get("startDate"),
82+ "image": images[0] if images else "",
83+ }
84+
85+ def fetch(self) -> list[Event]:
86+ html = self.get(LIST_URL).text
87+ paths = list(dict.fromkeys(_LINK_RE.findall(html)))
88+ cache = self.load_cache()
89+ now = time.time()
90+ current = set(paths)
91+ cache = {p: c for p, c in cache.items() if p in current}
92+
93+ to_fetch = sorted(
94+ (p for p in paths
95+ if p not in cache
96+ or now - cache[p].get("fetched_at", 0) > REFRESH_AFTER),
97+ key=lambda p: cache.get(p, {}).get("fetched_at", 0))[:CDE_FICHE_MAX]
98+
99+ def worker(path, session):
100+ return self._parse_fiche(
101+ path, session.get(BASE + path, timeout=20).text)
102+
103+ for path, row in self.fetch_many(
104+ to_fetch, worker, max_workers=4).items():
105+ if row is None: # fiche cassée : on ne bloque pas la source
106+ print(f"[sorti-ka] cantonsdelest : fiche ignorée {path}")
107+ continue
108+ cache[path] = {"fetched_at": now, "row": row}
109+ self.save_cache(cache)
110+
111+ events: list[Event] = []
112+ seen: set[str] = set()
113+ for entry in cache.values():
114+ row = entry.get("row")
115+ if not row or row["external_id"] in seen:
116+ continue
117+ seen.add(row["external_id"])
118+ events.append(Event(
119+ source=self.source_id,
120+ external_id=row["external_id"],
121+ url=row["url"],
122+ title=row["title"],
123+ description=row["description"],
124+ raw_categories=row["raw_categories"],
125+ address=row["address"],
126+ city=row["city"],
127+ postal_code=row["postal_code"],
128+ tourist_region="Cantons-de-l'Est",
129+ lat=row["lat"],
130+ lng=row["lng"],
131+ start_date=row["start_date"],
132+ end_date=row["end_date"],
133+ image=row["image"],
134+ ))
135+ return events
added sortika/connectors/eventbrite.py +209 −0
@@ -0,0 +1,209 @@
1+# -----------------------------------------------------------------------------
2+# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3+# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4+# connectors/eventbrite.py : connecteur Eventbrite (Québec) — SANS clé
5+# Source : eventbrite.ca — billetterie libre-service (communautaire,
6+# ateliers, festivals, affaires, vie nocturne...).
7+# Extraction: pages de découverte publiques /d/<lieu>/all-events/?page=N —
8+# chaque page embarque window.__SERVER_DATA__ avec les résultats
9+# de recherche COMPLETS (20 événements/page) : nom, résumé,
10+# date + heure locales, salle géocodée (lat/lng, adresse, code
11+# postal), image, catégories (tags Eventbrite*), URL. Vérifié
12+# 2026-08-18 : Montréal 3 341 événements (49 pages exposées).
13+# Prix : fiches détail /e/<slug> incrémentales (JSON-LD
14+# AggregateOffer lowPrice/highPrice CAD) avec cache disque
15+# data/eventbrite_cache.json, plafond EB_DETAIL_MAX fiches/sync
16+# (défaut 150), re-visite 7 jours — patron La Vitrine.
17+# Accès : robots.txt « User-agent: * » n'interdit ni /d/ ni /e/ (seuls
18+# /directory/, checkout et endpoints privés le sont) — GET
19+# directs throttlés, identification honnête, lien direct vers
20+# chaque fiche Eventbrite. Filtre region=QC ; en ligne exclus.
21+# Prix : uniquement l'AggregateOffer publié par Eventbrite — jamais
22+# inventé (gratuit seulement si high == 0).
23+# -----------------------------------------------------------------------------
24+from __future__ import annotations
25+
26+import json
27+import os
28+import re
29+import time
30+
31+from ..schema import Event
32+from .base import BaseConnector
33+
34+# lieux QC crawlés (slug découverte, budget de pages de liste par sync) —
35+# pondération par taille de marché ; ~30 GET de liste/sync ≈ 600 fiches
36+PLACES: tuple[tuple[str, int], ...] = (
37+ ("canada--montreal", 10),
38+ ("canada--qu%C3%A9bec", 6), # ville de Québec
39+ ("canada--laval", 3),
40+ ("canada--gatineau", 3),
41+ ("canada--sherbrooke", 3),
42+ ("canada--longueuil", 2),
43+ ("canada--trois-rivi%C3%A8res", 2),
44+)
45+LIST_URL = "https://www.eventbrite.ca/d/{place}/all-events/?page={page}"
46+EB_DETAIL_MAX = int(os.environ.get("EB_DETAIL_MAX", "150")) # fiches prix/sync
47+REFRESH_AFTER = 7 * 86400 # re-visite d'une fiche prix (secondes)
48+
49+_LD_JSON_RE = re.compile(r"application/ld\+json[^>]*>")
50+
51+
52+def _server_data(html: str) -> dict:
53+ i = html.find("__SERVER_DATA__")
54+ if i < 0:
55+ raise ValueError("__SERVER_DATA__ introuvable")
56+ start = html.find("{", i)
57+ data, _ = json.JSONDecoder().raw_decode(html[start:])
58+ return data
59+
60+
61+def _parse_offers(html: str) -> dict | None:
62+ """AggregateOffer (lowPrice/highPrice/priceCurrency) du JSON-LD d'une fiche."""
63+ for m in _LD_JSON_RE.finditer(html):
64+ try:
65+ d, _ = json.JSONDecoder().raw_decode(html[m.end():].lstrip())
66+ except Exception:
67+ continue
68+ for doc in (d if isinstance(d, list) else [d]):
69+ if not isinstance(doc, dict):
70+ continue
71+ offers = doc.get("offers")
72+ if not offers:
73+ continue
74+ for off in (offers if isinstance(offers, list) else [offers]):
75+ if not isinstance(off, dict) or off.get("@type") != "AggregateOffer":
76+ continue
77+ try:
78+ low = float(off.get("lowPrice"))
79+ high = float(off.get("highPrice") or off.get("lowPrice"))
80+ except (TypeError, ValueError):
81+ continue
82+ return {"low": low, "high": high,
83+ "cur": off.get("priceCurrency") or ""}
84+ return None
85+
86+
87+class EventbriteConnector(BaseConnector):
88+ source_id = "eventbrite"
89+ request_delay = 0.8
90+
91+ # -- pages de liste --------------------------------------------------------
92+ def _list_results(self) -> list[dict]:
93+ results: list[dict] = []
94+ for place, budget in PLACES:
95+ page_count = budget
96+ for page in range(1, budget + 1):
97+ if page > page_count:
98+ break
99+ try:
100+ data = _server_data(self.get(
101+ LIST_URL.format(place=place, page=page)).text)
102+ ev = ((data.get("search_data") or {}).get("events") or {})
103+ page_count = min(budget, int(
104+ (ev.get("pagination") or {}).get("page_count") or 0))
105+ results.extend(ev.get("results") or [])
106+ except Exception as exc: # un lieu cassé ne bloque pas la source
107+ print(f"[sorti-ka] eventbrite : {place} page {page} "
108+ f"ignorée : {exc}")
109+ break
110+ return results
111+
112+ # -- fiche → Event ---------------------------------------------------------
113+ def _result_to_event(self, r: dict, price: dict | None) -> Event | None:
114+ ext_id = str(r.get("id") or r.get("eventbrite_event_id") or "")
115+ title = r.get("name") or ""
116+ if not ext_id or not title:
117+ return None
118+ if r.get("is_online_event") or r.get("is_cancelled"):
119+ return None
120+ venue = r.get("primary_venue") or {}
121+ addr = venue.get("address") or {}
122+ if (addr.get("region") or "") != "QC": # filtre Québec strict
123+ return None
124+ cats: list[str] = []
125+ for t in r.get("tags") or []:
126+ if (t.get("prefix") or "").startswith("Eventbrite"):
127+ dn = ((t.get("localized") or {}).get("display_name")
128+ or t.get("display_name") or "")
129+ if dn and dn.lower() != "other" and dn not in cats:
130+ cats.append(dn)
131+ img = (((r.get("image") or {}).get("original") or {}).get("url")
132+ or (r.get("image") or {}).get("url") or "")
133+
134+ is_free, price_min, label = None, None, ""
135+ if price and price.get("cur") in ("CAD", ""):
136+ low, high = price["low"], price["high"]
137+ if high <= 0:
138+ is_free, label = True, "Gratuit (Eventbrite)"
139+ elif low <= 0: # options gratuites ET payantes
140+ label = f"0 $ à {high:g} $ (Eventbrite)"
141+ else:
142+ is_free, price_min = False, low
143+ label = (f"{low:g} $ à {high:g} $ (Eventbrite)"
144+ if high > low else f"{low:g} $ (Eventbrite)")
145+
146+ return Event(
147+ source=self.source_id,
148+ external_id=ext_id,
149+ url=r.get("url") or "",
150+ title=title,
151+ description=r.get("summary") or "",
152+ raw_categories=cats,
153+ venue=venue.get("name") or "",
154+ address=addr.get("address_1") or "",
155+ city=addr.get("city") or "",
156+ postal_code=addr.get("postal_code") or "",
157+ lat=addr.get("latitude"),
158+ lng=addr.get("longitude"),
159+ start_date=r.get("start_date"),
160+ start_time=r.get("start_time"),
161+ end_date=r.get("end_date"),
162+ is_free=is_free,
163+ price_min=price_min,
164+ price_label=label,
165+ image=img,
166+ )
167+
168+ # -- enrichissement prix (incrémental, plafonné) ---------------------------
169+ def _enrich_prices(self, rows: list[dict]) -> dict:
170+ """Cache {event_id: {fetched_at, price|None}} ; visite les fiches à
171+ venir sans prix connu d'abord, re-visite roulante après 7 jours."""
172+ cache = self.load_cache()
173+ now = time.time()
174+ current = {str(r.get("id")): r for r in rows if r.get("id")}
175+ cache = {k: v for k, v in cache.items() if k in current}
176+
177+ candidates = sorted(
178+ (eid for eid, r in current.items()
179+ if r.get("url")
180+ and (eid not in cache
181+ or now - cache[eid].get("fetched_at", 0) > REFRESH_AFTER)),
182+ key=lambda eid: cache.get(eid, {}).get("fetched_at", 0))[:EB_DETAIL_MAX]
183+ url_to_id = {current[eid]["url"]: eid for eid in candidates}
184+
185+ def worker(url, session):
186+ return _parse_offers(session.get(url, timeout=20).text)
187+
188+ for url, price in self.fetch_many(list(url_to_id), worker,
189+ max_workers=4).items():
190+ cache[url_to_id[url]] = {"fetched_at": now, "price": price}
191+ self.save_cache(cache)
192+ return {eid: (entry or {}).get("price") for eid, entry in cache.items()}
193+
194+ # -- interface --------------------------------------------------------------
195+ def fetch(self) -> list[Event]:
196+ rows = self._list_results()
197+ # dédoublonnage inter-lieux (un même événement sort dans plusieurs villes)
198+ uniq: dict[str, dict] = {}
199+ for r in rows:
200+ eid = str(r.get("id") or "")
201+ if eid:
202+ uniq.setdefault(eid, r)
203+ prices = self._enrich_prices(list(uniq.values()))
204+ events: list[Event] = []
205+ for eid, r in uniq.items():
206+ ev = self._result_to_event(r, prices.get(eid))
207+ if ev is not None:
208+ events.append(ev)
209+ return events
added sortika/connectors/longueuil.py +144 −0
@@ -0,0 +1,144 @@
1+# -----------------------------------------------------------------------------
2+# Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3+# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4+# connectors/longueuil.py : connecteur Ville de Longueuil — nouveau portail
5+# Source : longueuil.quebec/fr/evenements (portail Nuxt.js, rendu serveur).
6+# Extraction: cartes <article class="event__item"> de la page liste —
7+# <time datetime="2026-08-29T10:00:00-04:00"> (date + heure
8+# locales, parfois début ET fin), titre + lien, extrait.
9+# Enrichissement par fiche (incrémental, cache disque
10+# data/longueuil_cache.json, plafond LGL_FICHE_MAX/sync,
11+# re-visite 30 j) : adresse civique, code postal, municipalité
12+# extraits de l'état Nuxt (address_line1/postal_code/locality).
13+# GPS absent de la source → géocodeur central (salle + ville).
14+# Accès : portail municipal public, rendu serveur — GET directs
15+# throttlés, identification honnête. (L'ancien jeu « événements »
16+# sur Données Québec est périmé depuis 2017 — vérifié 2026-08-17 ;
17+# ce portail est la source vivante.)
18+# Prix : non publié dans la liste → jamais inventé.
19+# -----------------------------------------------------------------------------
20+from __future__ import annotations
21+
22+import os
23+import re
24+import time
25+
26+from ..normalize import clean_text, parse_date_iso, parse_time
27+from ..schema import Event
28+from .base import BaseConnector
29+
30+BASE = "https://longueuil.quebec"
31+LIST_URL = BASE + "/fr/evenements"
32+LGL_FICHE_MAX = int(os.environ.get("LGL_FICHE_MAX", "30")) # fiches/sync
33+REFRESH_AFTER = 30 * 86400 # l'adresse d'un événement bouge rarement
34+
35+_ARTICLE_RE = re.compile(r"<article class=\"event__item\".*?</article>", re.S)
36+_TIME_RE = re.compile(r"datetime=\"([^\"]+)\"")
37+_LINK_RE = re.compile(r"href=\"(/fr/evenements/[^\"]+)\"")
38+_TITLE_RE = re.compile(r"link__txt[^>]*>\s*(.*?)\s*</span>", re.S)
39+_EXCERPT_RE = re.compile(r"event__excerpt[^>]*>\s*(.*?)\s*</p>", re.S)
40+
41+# champs de l'état Nuxt des fiches (valeurs entre guillemets, échappées JS)
42+_ADDR_RE = re.compile(r"address_line1:\"([^\"]*)\"")
43+_POSTAL_RE = re.compile(r"postal_code:\"([^\"]*)\"")
44+_LOCALITY_RE = re.compile(r"locality:\"([^\"]*)\"")
45+
46+
47+def _js_str(raw: str) -> str:
48+ """Décode les échappements JS (\\u00e9 → é) d'une valeur d'état Nuxt."""
49+ if "\\u" in raw or "\\/" in raw:
50+ try:
51+ return raw.encode("utf-8").decode("unicode_escape")
52+ except Exception:
53+ return raw
54+ return raw
55+
56+
57+class LongueuilConnector(BaseConnector):
58+ source_id = "longueuil"
59+ request_delay = 0.8
60+
61+ def _cards(self, html: str) -> list[dict]:
62+ rows = []
63+ for block in _ARTICLE_RE.findall(html):
64+ link = _LINK_RE.search(block)
65+ title = _TITLE_RE.search(block)
66+ times = _TIME_RE.findall(block)
67+ if not (link and title and times):
68+ continue
69+ excerpt = _EXCERPT_RE.search(block)
70+ path = link.group(1)
71+ rows.append({
72+ "url": BASE + path,
73+ "external_id": path.rstrip("/").rsplit("/", 1)[-1],
74+ "title": clean_text(re.sub(r"<[^>]+>", "", title.group(1))),
75+ "description": clean_text(re.sub(
76+ r"<[^>]+>", "", excerpt.group(1))) if excerpt else "",
77+ "start": times[0],
78+ "end": times[-1],
79+ })
80+ return rows
81+
82+ def _enrich_addresses(self, rows: list[dict]) -> None:
83+ """Adresse/ville depuis la fiche (état Nuxt) — incrémental, plafonné."""
84+ cache = self.load_cache()
85+ now = time.time()
86+ current = {r["url"] for r in rows}
87+ cache = {u: c for u, c in cache.items() if u in current}
88+
89+ to_fetch = sorted(
90+ (u for u in current
91+ if u not in cache
92+ or now - cache[u].get("fetched_at", 0) > REFRESH_AFTER),
93+ key=lambda u: cache.get(u, {}).get("fetched_at", 0))[:LGL_FICHE_MAX]
94+
95+ def worker(url, session):
96+ html = session.get(url, timeout=20).text
97+ addr = _ADDR_RE.search(html)
98+ postal = _POSTAL_RE.search(html)
99+ loc = _LOCALITY_RE.search(html)
100+ return {"address": _js_str(addr.group(1)) if addr else "",
101+ "postal": _js_str(postal.group(1)) if postal else "",
102+ "city": _js_str(loc.group(1)) if loc else ""}
103+
104+ for url, info in self.fetch_many(to_fetch, worker, max_workers=4).items():
105+ if info is not None:
106+ info["fetched_at"] = now
107+ cache[url] = info
108+ self.save_cache(cache)
109+ for r in rows:
110+ info = cache.get(r["url"]) or {}
111+ r["address"] = info.get("address", "")
112+ r["postal"] = info.get("postal", "")
113+ r["city"] = info.get("city") or "Longueuil"
114+
115+ def fetch(self) -> list[Event]:
116+ rows = self._cards(self.get(LIST_URL).text)
117+ self._enrich_addresses(rows)
118+ events: list[Event] = []
119+ seen: set[str] = set()
120+ for r in rows:
121+ if r["external_id"] in seen:
122+ continue
123+ seen.add(r["external_id"])
124+ start_date = parse_date_iso(r["start"])
125+ end_date = parse_date_iso(r["end"])
126+ start_time = parse_time(r["start"])
127+ if start_time == "00:00": # minuit = « toute la journée »
128+ start_time = None
129+ events.append(Event(
130+ source=self.source_id,
131+ external_id=r["external_id"],
132+ url=r["url"],
133+ title=r["title"],
134+ description=r["description"],
135+ raw_categories=[r["title"]],
136+ address=r.get("address", ""),
137+ postal_code=r.get("postal", ""),
138+ city=r.get("city", "Longueuil"),
139+ region="Montérégie",
140+ start_date=start_date,
141+ start_time=start_time,
142+ end_date=end_date or start_date,
143+ ))
144+ return events
modified sortika/connectors/ticketmaster.py +157 −22
@@ -1,32 +1,54 @@
1 # ----------------------------------------------------------------------------- 1 # -----------------------------------------------------------------------------
2 # Sorti-Ka — Agrégateur de sorties & événements (province de Québec) 2 # Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4−# connectors/ticketmaster.py : connecteur Ticketmaster Discovery API (Québec) 4+# connectors/ticketmaster.py : connecteur Ticketmaster Canada (Québec) — SANS clé
5−# Source : app.ticketmaster.com/discovery/v2/events — API officielle 5+# Mode principal (sans clé) : scraping léger des pages découverte publiques
6−# publique (clé gratuite sur developer.ticketmaster.com). 6+# www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city,
7−# Extraction: GET JSON paginé (countryCode=CA & stateCode=QC, size=200) — 7+# gatineau). Chaque page embarque son état Next.js (__NEXT_DATA__) : la
8−# nom, dates + heure locale, salle géolocalisée (venues lat/lng), 8+# requête serveur `cityEvents` y est déjà résolue avec 20 événements/page
9−# fourchette de prix (priceRanges min/max), images 16:9, 9+# (titre, date UTC + fuseau, salle géolocalisée lat/lng, adresse, code
10−# classifications (segment/genre) → catégories, attractions → 10+# postal, ville, URL billetterie, catégorie majeure, seatmap). Pagination
11−# artistes, URL de billetterie officielle. 11+# par ?page=N (vérifié 2026-08-18 : montreal total=1165, quebec-city=136).
12−# Accès : API officielle avec clé — quota 5 req/s & 5 000 req/jour 12+# Aucun prix publié dans ce flux → is_free/price_* inconnus (jamais
13−# (throttle 0,25 s ; ~10 requêtes/sync, pagination plafonnée par 13+# inventés). Artistes : patrons sûrs des titres (artists_from_title).
14−# l'API à 1 000 éléments par requête profonde). 14+# robots.txt : /discover/ non interdit (seuls /api/ismds/* le sont).
15−# Clé : TICKETMASTER_API_KEY dans .env (voir .env.example). Sans clé, 15+# Anti-bot : GET direct d'abord ; si bloqué → Scrapfly ASP SANS render_js.
16−# le connecteur s'annonce « clé manquante » et ne retourne rien — 16+# Plafonds : TM_MAX_EVENTS (défaut 360), TM_PAGES_PER_CITY (défaut 8).
17−# statut « prêt — clé requise » dans data/sources.json. 17+# Mode repli automatique (si TICKETMASTER_API_KEY apparaît dans .env) :
18+# Discovery API officielle v2 — plus riche (priceRanges, attractions →
19+# artistes, images 16:9). Les identifiants d'événements sont IDENTIQUES
20+# dans les deux modes → aucun doublon lors d'un changement de mode.
18 # ----------------------------------------------------------------------------- 21 # -----------------------------------------------------------------------------
19 from __future__ import annotations 22 from __future__ import annotations
20 23
24+import json
21 import os 25 import os
22 26
27+from ..normalize import artists_from_title, utc_to_local
23 from ..schema import Event 28 from ..schema import Event
24 from .base import BaseConnector 29 from .base import BaseConnector
25 30
31+# --- mode API officielle (avec clé) ------------------------------------------
26 API = "https://app.ticketmaster.com/discovery/v2/events.json" 32 API = "https://app.ticketmaster.com/discovery/v2/events.json"
27 PAGE_SIZE = 200 33 PAGE_SIZE = 200
28 DEEP_PAGE_LIMIT = 1000 # l'API refuse page*size >= 1000 (pagination profonde) 34 DEEP_PAGE_LIMIT = 1000 # l'API refuse page*size >= 1000 (pagination profonde)
29 35
36+# --- mode scraping (sans clé) -------------------------------------------------
37+DISCOVER = "https://www.ticketmaster.ca/discover/{city}?page={page}"
38+CITIES = ("montreal", "quebec-city", "gatineau") # villes découverte QC publiées
39+TM_MAX_EVENTS = int(os.environ.get("TM_MAX_EVENTS", "360"))
40+TM_PAGES_PER_CITY = int(os.environ.get("TM_PAGES_PER_CITY", "8"))
41+SCRAPE_PAGE_SIZE = 20 # taille fixe du flux cityEvents des pages découverte
42+
43+# catégories majeures Ticketmaster (segments Discovery) → libellés source
44+_CAT_NAMES = {
45+ "KZFzniwnSyZfZ7v7nJ": "Concerts",
46+ "KZFzniwnSyZfZ7v7na": "Arts & Theatre",
47+ "KZFzniwnSyZfZ7v7nE": "Sports",
48+ "KZFzniwnSyZfZ7v7n1": "Family",
49+ "KZFzniwnSyZfZ7v7n7": "Film",
50+}
51+
30 52
31 def _best_image(images: list[dict]) -> str: 53 def _best_image(images: list[dict]) -> str:
32 """Plus grande image 16:9 (sinon la plus large disponible).""" 54 """Plus grande image 16:9 (sinon la plus large disponible)."""
@@ -39,8 +61,119 @@ def _best_image(images: list[dict]) -> str:
39 61
40 class TicketmasterConnector(BaseConnector): 62 class TicketmasterConnector(BaseConnector):
41 source_id = "ticketmaster" 63 source_id = "ticketmaster"
42− request_delay = 0.25 # max 5 req/s (quota officiel) 64+ request_delay = 1.0 # politesse scraping ; l'API tolère 5 req/s
65+
66+ def __init__(self) -> None:
67+ super().__init__()
68+ self._force_scrapfly = False
69+ self.session.headers["Accept-Language"] = "fr-CA,fr;q=0.9,en;q=0.5"
70+
71+ # ==========================================================================
72+ # Mode scraping SANS clé (pages découverte publiques)
73+ # ==========================================================================
74+ def _discover_html(self, url: str) -> str:
75+ """HTML d'une page découverte : GET direct, Scrapfly ASP en secours."""
76+ if not self._force_scrapfly:
77+ try:
78+ html = self.get(url).text
79+ if "__NEXT_DATA__" in html:
80+ return html
81+ except Exception:
82+ pass
83+ if not os.environ.get("SCRAPFLY_API_KEY"):
84+ raise RuntimeError("GET direct bloqué et SCRAPFLY_API_KEY absent")
85+ self._force_scrapfly = True
86+ print("[sorti-ka] ticketmaster : GET direct bloqué → "
87+ "Scrapfly ASP (sans render_js)")
88+ return self.get_rendered(url, render_js=False)
89+
90+ @staticmethod
91+ def _next_data(html: str) -> dict:
92+ i = html.find("__NEXT_DATA__")
93+ if i < 0:
94+ raise ValueError("__NEXT_DATA__ introuvable (anti-bot ?)")
95+ start = html.find(">", i) + 1
96+ data, _ = json.JSONDecoder().raw_decode(html[start:].lstrip())
97+ return data
98+
99+ def _city_page(self, city: str, page: int) -> tuple[list[dict], int]:
100+ """Événements (hits) + total annoncé d'une page découverte."""
101+ data = self._next_data(self._discover_html(
102+ DISCOVER.format(city=city, page=page)))
103+ queries = (((data.get("props") or {}).get("pageProps") or {})
104+ .get("initialReduxState") or {}).get("api", {}).get("queries", {})
105+ hits: list[dict] = []
106+ total = 0
107+ for key, q in queries.items():
108+ if key.startswith("cityEvents(") and f'"page":{page}' in key:
109+ d = q.get("data") or {}
110+ hits.extend(d.get("events") or [])
111+ total = max(total, int(d.get("total") or 0))
112+ return hits, total
43 113
114+ def _scrape_hit_to_event(self, hit: dict) -> Event | None:
115+ ext_id = hit.get("id") or ""
116+ title = hit.get("title") or ""
117+ if not ext_id or not title:
118+ return None
119+ if hit.get("cancelled") or hit.get("virtual"):
120+ return None
121+ v = hit.get("venue") or {}
122+ dates = hit.get("dates") or {}
123+ # startDate publié en UTC + fuseau → date/heure locales Québec ;
124+ # heure retenue seulement si la source l'affiche (showDateTime, pas TBA)
125+ start_date, start_time = utc_to_local(dates.get("startDate"))
126+ if hit.get("tba") or dates.get("dateDisplay") != "showDateTime":
127+ start_time = None
128+ cat_id = (hit.get("majorCategory") or {}).get("id") or ""
129+ venue_name = v.get("name") or ""
130+ return Event(
131+ source=self.source_id,
132+ external_id=ext_id,
133+ url=hit.get("url") or "",
134+ title=title,
135+ raw_categories=[_CAT_NAMES[cat_id]] if cat_id in _CAT_NAMES else [],
136+ venue=venue_name,
137+ address=v.get("addressLineOne") or "",
138+ city=v.get("city") or "",
139+ postal_code=v.get("code") or "",
140+ lat=v.get("latitude"),
141+ lng=v.get("longitude"),
142+ start_date=start_date,
143+ start_time=start_time,
144+ end_date=start_date,
145+ artists=artists_from_title(title, venue_name),
146+ # seul visuel publié dans ce flux : le plan de salle officiel
147+ image=hit.get("seatmapUrl") or "",
148+ )
149+
150+ def _fetch_scrape(self) -> list[Event]:
151+ by_id: dict[str, Event] = {}
152+ for city in CITIES:
153+ for page in range(TM_PAGES_PER_CITY):
154+ if len(by_id) >= TM_MAX_EVENTS:
155+ break
156+ try:
157+ hits, total = self._city_page(city, page)
158+ except Exception as exc:
159+ print(f"[sorti-ka] ticketmaster : {city} page {page} "
160+ f"ignorée : {exc}")
161+ break
162+ if not hits:
163+ break
164+ for hit in hits:
165+ ev = self._scrape_hit_to_event(hit)
166+ if ev is not None:
167+ by_id.setdefault(ev.external_id, ev)
168+ if total and (page + 1) * SCRAPE_PAGE_SIZE >= total:
169+ break
170+ if len(by_id) >= TM_MAX_EVENTS:
171+ break
172+ return list(by_id.values())
173+
174+ # ==========================================================================
175+ # Mode API officielle (repli automatique dès qu'une clé est fournie)
176+ # ==========================================================================
44 def _page(self, key: str, page: int) -> dict: 177 def _page(self, key: str, page: int) -> dict:
45 return self.get_json(API, params={ 178 return self.get_json(API, params={
46 "apikey": key, "countryCode": "CA", "stateCode": "QC", 179 "apikey": key, "countryCode": "CA", "stateCode": "QC",
@@ -102,13 +235,8 @@ class TicketmasterConnector(BaseConnector):
102 image=_best_image(hit.get("images") or []), 235 image=_best_image(hit.get("images") or []),
103 ) 236 )
104 237
105− def fetch(self) -> list[Event]: 238+ def _fetch_api(self, key: str) -> list[Event]:
106− key = os.environ.get("TICKETMASTER_API_KEY", "").strip() 239+ self.request_delay = 0.25 # quota officiel : 5 req/s
107− if not key:
108− print("[sorti-ka] ticketmaster : clé manquante "
109− "(TICKETMASTER_API_KEY dans .env — gratuite sur "
110− "developer.ticketmaster.com) — connecteur prêt, sync sautée")
111− return []
112 events: list[Event] = [] 240 events: list[Event] = []
113 page, total_pages = 0, 1 241 page, total_pages = 0, 1
114 while page < total_pages and (page + 1) * PAGE_SIZE <= DEEP_PAGE_LIMIT: 242 while page < total_pages and (page + 1) * PAGE_SIZE <= DEEP_PAGE_LIMIT:
@@ -120,3 +248,10 @@ class TicketmasterConnector(BaseConnector):
120 events.append(ev) 248 events.append(ev)
121 page += 1 249 page += 1
122 return events 250 return events
251+
252+ # ==========================================================================
253+ def fetch(self) -> list[Event]:
254+ key = os.environ.get("TICKETMASTER_API_KEY", "").strip()
255+ if key:
256+ return self._fetch_api(key) # mode riche dès qu'une clé existe
257+ return self._fetch_scrape() # mode sans clé (par défaut)
modified tests/test_connectors.py +42 −3
@@ -170,10 +170,49 @@ def test_sherbrooke_times_converted_to_local(monkeypatch):
170 assert "07:30" in times or "20:00" in times 170 assert "07:30" in times or "20:00" in times
171 171
172 172
173−def test_ticketmaster_without_key_is_ready_but_skipped(monkeypatch): 173+def test_ticketmaster_without_key_scrapes_discover(monkeypatch):
174− from sortika.connectors.ticketmaster import TicketmasterConnector 174+ """Sans clé, le connecteur scrape les pages découverte (état cityEvents) :
175+ hit → Event avec date UTC convertie en locale, salle géolocalisée."""
176+ from sortika.connectors import ticketmaster as tm
175 monkeypatch.delenv("TICKETMASTER_API_KEY", raising=False) 177 monkeypatch.delenv("TICKETMASTER_API_KEY", raising=False)
176− assert TicketmasterConnector().fetch() == [] # « clé manquante », pas d'erreur 178+ hit = {
179+ "id": "310064A20EB06FDE", "title": "Mardi Latin Groove",
180+ "url": "https://www.ticketmaster.ca/x/event/310064A20EB06FDE",
181+ "dates": {"dateDisplay": "showDateTime",
182+ "startDate": "2026-08-18T23:00:00Z"},
183+ "seatmapUrl": "https://mapsapi.tmol.io/maps/x.png",
184+ "venue": {"name": "Le Balcon X Terrasse", "city": "Montreal",
185+ "state": "QC", "latitude": 45.50543786,
186+ "longitude": -73.56881164,
187+ "addressLineOne": "463 Rue Sainte-Catherine Ouest",
188+ "code": "H3B1B1"},
189+ "majorCategory": {"id": "KZFzniwnSyZfZ7v7nJ"},
190+ }
191+ c = tm.TicketmasterConnector()
192+ monkeypatch.setattr(c, "_city_page",
193+ lambda city, page: ([hit], 1) if page == 0 else ([], 1))
194+ events = [e.finalize() for e in c.fetch()]
195+ # 1 seul événement malgré les 3 villes crawlées (dédoublonnage par id)
196+ assert len(events) == 1
197+ ev = events[0]
198+ assert ev.external_id == "310064A20EB06FDE"
199+ assert ev.start_date == "2026-08-18" and ev.start_time == "19:00" # UTC→locale
200+ assert ev.venue == "Le Balcon X Terrasse" and ev.city == "Montreal"
201+ assert ev.lat == 45.505438 and ev.region == "Montréal"
202+ assert ev.categories # Concerts → taxonomie canonique
203+ assert ev.is_free is None and ev.price_min is None # jamais inventé
204+
205+
206+def test_ticketmaster_scrape_skips_cancelled_and_tba_time():
207+ from sortika.connectors.ticketmaster import TicketmasterConnector
208+ c = TicketmasterConnector()
209+ assert c._scrape_hit_to_event({"id": "x", "title": "T",
210+ "cancelled": True}) is None
211+ ev = c._scrape_hit_to_event({
212+ "id": "y", "title": "Sans heure annoncée",
213+ "dates": {"dateDisplay": "showDateOnly",
214+ "startDate": "2026-09-01T04:00:00Z"}, "venue": {}})
215+ assert ev is not None and ev.start_time is None
177 216
178 217
179 def test_ticketmaster_parses_hit(): 218 def test_ticketmaster_parses_hit():
180 219
Menu

Theme