SPB Git forge

spb/sorti-ka

Public

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

58commits 1branches 0releases
13.7 MBsize
maindefault branch
17 days agolast push
HTML 82.9% Python 15.2% TypeScript 0.9% JavaScript 0.7%

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 1 mo ago (Aug 18, 2026) parent 332cc93

27 changed files +1,224 −232

modified data/sources.json +38 −16
@@ -58,15 +58,37 @@
58 58 },
59 59 {
60 60 "id": "longueuil",
61 − "name": "Ville de Longueuil — Événements",
62 − "url": "https://www.donneesquebec.ca/recherche/dataset/evenements",
63 − "type": "donnees-ouvertes",
64 − "extraction": "CSV (Données Québec)",
65 − "acces_legal": "Licence CC 4.0 (Données Québec)",
66 − "couverture": "Longueuil (Montérégie)",
67 − "cadence": "n/d",
68 − "statut": "abandonnee",
69 − "note": "Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17)"
61 + "name": "Ville de Longueuil — Événements (nouveau portail)",
62 + "url": "https://longueuil.quebec/fr/evenements",
63 + "type": "municipal",
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": "portail municipal public, rendu serveur — GET throttlés 0,8 s, identification honnête",
66 + "couverture": "Événements municipaux, Longueuil (Montérégie)",
67 + "cadence": "horaire (watcher PM2, incrémental)",
68 + "statut": "actif",
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 94 "id": "saguenay",
@@ -141,12 +163,12 @@
141 163 "name": "Ticketmaster — Discovery API (Québec)",
142 164 "url": "https://developer.ticketmaster.com/products-and-docs/apis/discovery-api/v2/",
143 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)",
145 − "acces_legal": "API officielle publique avec clé gratuite (developer.ticketmaster.com) — conditions Ticketmaster Developer respectées",
146 − "couverture": "Grands spectacles, sports et concerts — partout au Québec",
147 − "cadence": "horaire (watcher PM2) dès que la clé est fournie",
148 − "statut": "prêt — clé requise",
149 − "note": "Nécessite TICKETMASTER_API_KEY dans .env (clé gratuite). Sans clé, le connecteur s'annonce « clé manquante » et ne retourne rien."
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.",
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",
168 + "couverture": "Grands spectacles, sports et concerts — Montréal/Laval/Brossard, Québec, Gatineau",
169 + "cadence": "horaire (watcher PM2)",
170 + "statut": "actif",
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 174 "id": "bandsintown",
@@ -208,4 +230,4 @@
208 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 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 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 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 |
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 |
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 |
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 |
17 −| [Ville de Sherbrooke — Calendrier des événements](sherbrooke.md) | municipal | actif | 271 | 0 % | 100 % | 88 % | 0 % | 100 % | 0 % | 2026-08-18 02:41 |
18 −| [evenko — grands concerts et festivals](evenko.md) | promoteur | actif | 423 | 77 % | 0 % | 99 % | 99 % | 100 % | 100 % | 2026-08-18 02:35 |
19 −| [Atuvu.ca — calendrier culturel](atuvu.md) | agregateur | actif | 145 | 62 % | 35 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 02:31 |
20 −| [La Vitrine culturelle](lavitrine.md) | billetterie | actif | 5127 | 52 % | 0 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 02:35 |
21 −| [Ville de Brossard — Calendrier des événements](brossard.md) | municipal | actif | 262 | 0 % | 0 % | 54 % | 0 % | 55 % | 53 % | 2026-08-18 02:35 |
22 −| [Ticketmaster — Discovery API (Québec)](ticketmaster.md) | billetterie | prêt — clé requise | 0 | — | — | — | — | — | — | 2026-08-18 02:41 |
23 −| [Bandsintown — dates de concerts par artiste](bandsintown.md) | agregateur | actif | 251 | 100 % | 1 % | 100 % | 100 % | 10 % | 100 % | 2026-08-18 02:32 |
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 % | 21 % | 0 % | 58 % | 4 % | 2026-08-18 05:01 |
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 04:55 |
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 +| [Ville de Longueuil — Événements (nouveau portail)](longueuil.md) | municipal | actif | 9 | 0 % | 0 % | 100 % | 0 % | 0 % | 0 % | 2026-08-18 05:52 |
19 +| [Eventbrite — billetterie libre-service (Québec)](eventbrite.md) | billetterie | actif | 426 | 100 % | 65 % | 100 % | 0 % | 97 % | 100 % | 2026-08-18 05:52 |
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 +| [evenko — grands concerts et festivals](evenko.md) | promoteur | actif | 421 | 77 % | 0 % | 99 % | 99 % | 100 % | 100 % | 2026-08-18 04:55 |
22 +| [Atuvu.ca — calendrier culturel](atuvu.md) | agregateur | actif | 145 | 62 % | 35 % | 0 % | 0 % | 100 % | 100 % | 2026-08-18 04:52 |
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 28 ## Sources abandonnées
26 29
27 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 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 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 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 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 5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/atuvu.py`
6 6
@@ -38,28 +38,28 @@
38 38
39 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 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 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 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 50 | `address` — Adresse | adresse civique | 0 % | ∅ |
51 −| `city` — Ville | municipalité | 97 % | Montréal |
52 −| `region` — Région | région administrative | 93 % | Montréal |
53 −| `lat` — GPS (lat/lng) | coordonnées géographiques | 62 % | 45.5044365, -73.5559901 |
54 −| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-11-07 |
55 −| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-11-07 |
51 +| `city` — Ville | municipalité | 97 % | Québec |
52 +| `region` — Région | région administrative | 93 % | Capitale-Nationale |
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-08-12 |
55 +| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-12 |
56 56 | `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 0 % | ∅ |
57 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 |
59 −| `price_label` — Libellé prix | étiquette tarifaire affichable | 29 % | 54.58 $ (prix régulier atuvu.ca) |
58 +| `price_min` — Prix min | tarif minimal réel ($) | 35 % | 47.66 |
59 +| `price_label` — Libellé prix | étiquette tarifaire affichable | 29 % | 47.66 $ (prix régulier atuvu.ca) |
60 60 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
61 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 63 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
64 64
65 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 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 79 - **Événements en base** : 145 (dont **145 actifs**)
80 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 83 - **Artistes** : 0/145 actifs (0 %)
84 84 - **Image** : 145/145 actifs (100 %)
85 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
87 −- **Syncs enregistrés** (sync_log) : 16
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) : 18
88 88
89 89 ## Erreurs & dépannage
90 90
modified docs/connecteurs/bandsintown.md +24 −24
@@ -1,6 +1,6 @@
1 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 5 **Statut : actif** · Type : `agregateur` · Connecteur : `sortika/connectors/bandsintown.py`
6 6
@@ -43,28 +43,28 @@
43 43
44 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 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 |
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… |
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&… |
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) | 17 % | ∅ |
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 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 |
55 −| `address` — Adresse | adresse civique | 98 % | 21 Rue Tri Jean |
56 −| `city` — Ville | municipalité | 100 % | Grenville |
57 −| `region` — Région | région administrative | 98 % | Laurentides |
58 −| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.630063, -74.600568 |
54 +| `venue` — Salle (venue) | nom du lieu | 100 % | Saint-Roch-de-l'Achigan en Fête 2026 |
55 +| `address` — Adresse | adresse civique | 98 % | Rue Vézina |
56 +| `city` — Ville | municipalité | 100 % | Saint-roch-de-l'achigan |
57 +| `region` — Région | région administrative | 98 % | Lanaudière |
58 +| `lat` — GPS (lat/lng) | coordonnées géographiques | 100 % | 45.860801, -73.593176 |
59 59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-22 |
60 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 62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 1 % | 1 |
63 63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
64 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 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 68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
69 69
70 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 81 ## Volumétrie & complétude live
82 82
83 −*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)*
84 −
85 −- **Événements en base** : 251 (dont **251 actifs**)
86 −- **GPS** : 251/251 actifs (100 %)
87 −- **Prix** : 2/251 actifs (1 %)
88 −- **Heure (start_time)** : 250/251 actifs (100 %)
89 −- **Artistes** : 251/251 actifs (100 %)
90 −- **Image** : 24/251 actifs (10 %)
91 −- **Salle (venue)** : 251/251 actifs (100 %)
92 −- **Dernier sync** : 2026-08-18 02:32 — fetched 251, +143 ajoutés, ~0 mis à jour, -0 retirés
93 −- **Syncs enregistrés** (sync_log) : 2
83 +*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
84 +
85 +- **Événements en base** : 372 (dont **372 actifs**)
86 +- **GPS** : 372/372 actifs (100 %)
87 +- **Prix** : 4/372 actifs (1 %)
88 +- **Heure (start_time)** : 371/372 actifs (100 %)
89 +- **Artistes** : 372/372 actifs (100 %)
90 +- **Image** : 29/372 actifs (8 %)
91 +- **Salle (venue)** : 372/372 actifs (100 %)
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) : 4
94 94
95 95 ## Erreurs & dépannage
96 96
modified docs/connecteurs/brossard.md +17 −17
@@ -1,6 +1,6 @@
1 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 5 **Statut : actif** · Type : `municipal` · Connecteur : `sortika/connectors/brossard.py`
6 6
@@ -43,7 +43,7 @@
43 43
44 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 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 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 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 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 |
55 −| `address` — Adresse | adresse civique | 53 % | 7855, av San Francisco |
54 +| `venue` — Salle (venue) | nom du lieu | 54 % | Bibliothèque de Brossard Georgette-Lepage |
55 +| `address` — Adresse | adresse civique | 54 % | 7855, av San Francisco |
56 56 | `city` — Ville | municipalité | 100 % | Brossard |
57 57 | `region` — Région | région administrative | 100 % | Montérégie |
58 58 | `lat` — GPS (lat/lng) | coordonnées géographiques | 0 % | 45.4551469, -73.4682277 |
59 59 | `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-26 |
60 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 62 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 0 % | ∅ |
63 63 | `price_min` — Prix min | tarif minimal réel ($) | 0 % | ∅ |
64 64 | `price_label` — Libellé prix | étiquette tarifaire affichable | 0 % | ∅ |
65 65 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
66 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 68 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
69 69
70 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 80 ## Volumétrie & complétude live
81 81
82 −*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)*
83 −
84 −- **Événements en base** : 262 (dont **262 actifs**)
85 −- **GPS** : 1/262 actifs (0 %)
86 −- **Prix** : 0/262 actifs (0 %)
87 −- **Heure (start_time)** : 141/262 actifs (54 %)
88 −- **Artistes** : 0/262 actifs (0 %)
89 −- **Image** : 145/262 actifs (55 %)
90 −- **Salle (venue)** : 138/262 actifs (53 %)
91 −- **Dernier sync** : 2026-08-18 02:35 — fetched 262, +11 ajoutés, ~56 mis à jour, -0 retirés
92 −- **Syncs enregistrés** (sync_log) : 2
82 +*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
83 +
84 +- **Événements en base** : 264 (dont **264 actifs**)
85 +- **GPS** : 1/264 actifs (0 %)
86 +- **Prix** : 0/264 actifs (0 %)
87 +- **Heure (start_time)** : 145/264 actifs (55 %)
88 +- **Artistes** : 0/264 actifs (0 %)
89 +- **Image** : 151/264 actifs (57 %)
90 +- **Salle (venue)** : 142/264 actifs (54 %)
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) : 4
93 93
94 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 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 5 **Statut : actif** · Type : `promoteur` · Connecteur : `sortika/connectors/evenko.py`
6 6
@@ -40,7 +40,7 @@
40 40
41 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 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 72 ## Volumétrie & complétude live
73 73
74 −*(mesuré le 2026-08-18 03:22 EDT dans `data/sortika.db`)*
75 −
76 −- **Événements en base** : 423 (dont **423 actifs**)
77 −- **GPS** : 327/423 actifs (77 %)
78 −- **Prix** : 0/423 actifs (0 %)
79 −- **Heure (start_time)** : 420/423 actifs (99 %)
80 −- **Artistes** : 420/423 actifs (99 %)
81 −- **Image** : 423/423 actifs (100 %)
82 −- **Salle (venue)** : 423/423 actifs (100 %)
83 −- **Dernier sync** : 2026-08-18 02:35 — fetched 423, +0 ajoutés, ~9 mis à jour, -0 retirés
84 −- **Syncs enregistrés** (sync_log) : 18
74 +*(mesuré le 2026-08-18 05:52 EDT dans `data/sortika.db`)*
75 +
76 +- **Événements en base** : 423 (dont **421 actifs**)
77 +- **GPS** : 326/421 actifs (77 %)
78 +- **Prix** : 0/421 actifs (0 %)
79 +- **Heure (start_time)** : 418/421 actifs (99 %)
80 +- **Artistes** : 418/421 actifs (99 %)
81 +- **Image** : 421/421 actifs (100 %)
82 +- **Salle (venue)** : 421/421 actifs (100 %)
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) : 20
85 85
86 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 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 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 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 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 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 69 - **Événements en base** : 277 (dont **277 actifs**)
70 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 73 - **Artistes** : 0/277 actifs (0 %)
74 74 - **Image** : 271/277 actifs (98 %)
75 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
77 −- **Syncs enregistrés** (sync_log) : 21
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) : 23
78 78
79 79 ## Erreurs & dépannage
80 80
modified docs/connecteurs/lavitrine.md +4 −4
@@ -1,6 +1,6 @@
1 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 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 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 82 - **Événements en base** : 5137 (dont **5127 actifs**)
83 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 86 - **Artistes** : 0/5127 actifs (0 %)
87 87 - **Image** : 5127/5127 actifs (100 %)
88 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
90 −- **Syncs enregistrés** (sync_log) : 11
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) : 13
91 91
92 92 ## Erreurs & dépannage
93 93
modified docs/connecteurs/lepointdevente.md +18 −18
@@ -1,6 +1,6 @@
1 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 5 **Statut : actif** · Type : `billetterie` · Connecteur : `sortika/connectors/lepointdevente.py`
6 6
@@ -44,28 +44,28 @@
44 44
45 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 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 |
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… |
53 −| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://lepointdevente.com/billets/98a260908001 |
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) | 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/ghj260915003 |
54 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 56 | `address` — Adresse | adresse civique | 0 % | ∅ |
57 57 | `city` — Ville | municipalité | 100 % | Québec |
58 58 | `region` — Région | région administrative | 98 % | Capitale-Nationale |
59 −| `lat` — GPS (lat/lng) | coordonnées géographiques | 27 % | 46.2897843, -72.6724193 |
60 −| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-09-08 |
61 −| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-09-08 |
62 −| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 94 % | 19:00 |
63 −| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 31 % | 1 |
64 −| `price_min` — Prix min | tarif minimal réel ($) | 29 % | ∅ |
65 −| `price_label` — Libellé prix | étiquette tarifaire affichable | 31 % | Admission générale 0 $ |
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-15 |
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 % | 17:00 |
63 +| `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 46 % | 0 |
64 +| `price_min` — Prix min | tarif minimal réel ($) | 45 % | 261.26 |
65 +| `price_label` — Libellé prix | étiquette tarifaire affichable | 46 % | Admission générale 261.26 $ |
66 66 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 1 % | [] |
67 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 69 | `website` — Site web | site officiel de l'événement | 0 % | ∅ |
70 70
71 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 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 85 - **Événements en base** : 4191 (dont **1787 actifs**)
86 86 - **GPS** : 476/1787 actifs (27 %)
87 −- **Prix** : 547/1787 actifs (31 %)
87 +- **Prix** : 823/1787 actifs (46 %)
88 88 - **Heure (start_time)** : 1673/1787 actifs (94 %)
89 89 - **Artistes** : 16/1787 actifs (1 %)
90 90 - **Image** : 1787/1787 actifs (100 %)
91 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
93 −- **Syncs enregistrés** (sync_log) : 22
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) : 24
94 94
95 95 ## Erreurs & dépannage
96 96
modified docs/connecteurs/levis.md +1 −1
@@ -1,6 +1,6 @@
1 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 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 7 ## Description
8 8
9 −- **Source** : [Ville de Longueuil — Événements](https://www.donneesquebec.ca/recherche/dataset/evenements)
10 −- **Couverture** : Longueuil (Montérégie)
11 −- **Extraction** : CSV (Données Québec)
12 −- **Note** : Jeu de données périmé — dernière date d'événement : 2017-03 (vérifié 2026-08-17)
9 +- **Source** : [Ville de Longueuil — Événements (nouveau portail)](https://longueuil.quebec/fr/evenements)
10 +- **Couverture** : Événements municipaux, Longueuil (Montérégie)
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** : 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 14 ## Accès
15 15
16 −- **Type** : donnees-ouvertes
17 −- **Endpoint** : voir URL source (https://www.donneesquebec.ca/recherche/dataset/evenements)
16 +- **Type** : municipal
17 +- **Endpoint** (`BASE`) : `https://longueuil.quebec`
18 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 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 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 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 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 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 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 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 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 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 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 5 **Statut : actif** · Type : `donnees-ouvertes` · Connecteur : `sortika/connectors/montreal.py`
6 6
@@ -37,25 +37,25 @@
37 37
38 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 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é |
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… |
46 −| `url` — URL | lien vers la fiche originale / billetterie | 100 % | https://montreal.ca/evenements/les-legendaires-cine-ete-111611 |
47 −| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["cinema"] |
48 −| `venue` — Salle (venue) | nom du lieu | 4 % | Cinéma Beaubien |
49 −| `address` — Adresse | adresse civique | 4 % | 2396 Rue Beaubien E |
44 +| `title` — Titre | titre de l'événement | 100 % | Swing Tonique Jazz Band |
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/swing-tonique-jazz-band-114189 |
47 +| `categories` — Catégories | taxonomie canonique Sorti-Ka (JSON) | 100 % | ["musique"] |
48 +| `venue` — Salle (venue) | nom du lieu | 4 % | Plaza St-Hubert |
49 +| `address` — Adresse | adresse civique | 4 % | Coin rue Bellechasse et rue St-Hubert |
50 50 | `city` — Ville | municipalité | 100 % | Montréal |
51 51 | `region` — Région | région administrative | 100 % | Montréal |
52 −| `lat` — GPS (lat/lng) | coordonnées géographiques | 97 % | 45.548115, -73.5921 |
53 −| `start_date` — Date début | date locale AAAA-MM-JJ | 100 % | 2026-08-22 |
54 −| `end_date` — Date fin | date locale AAAA-MM-JJ | 100 % | 2026-08-22 |
55 −| `start_time` — Heure (start_time) | heure locale HH:MM (Québec) | 10 % | 09:30 |
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-20 |
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) | 21 % | 17:30 |
56 56 | `is_free` — Gratuité (is_free) | 1 gratuit / 0 payant / NULL inconnu | 100 % | 1 |
57 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 59 | `artists` — Artistes | têtes d'affiche / lineup (JSON) | 0 % | [] |
60 60 | `organizer` — Organisateur | promoteur / organisme | 100 % | Ville de Montréal — Rosemont–La Petite-Patrie |
61 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 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 78 - **Événements en base** : 5729 (dont **5679 actifs**)
79 79 - **GPS** : 5530/5679 actifs (97 %)
80 80 - **Prix** : 5679/5679 actifs (100 %)
81 −- **Heure (start_time)** : 588/5679 actifs (10 %)
81 +- **Heure (start_time)** : 1187/5679 actifs (21 %)
82 82 - **Artistes** : 0/5679 actifs (0 %)
83 83 - **Image** : 3296/5679 actifs (58 %)
84 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
86 −- **Syncs enregistrés** (sync_log) : 22
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) : 24
87 87
88 88 ## Erreurs & dépannage
89 89
modified docs/connecteurs/repentigny.md +1 −1
@@ -1,6 +1,6 @@
1 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 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 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 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 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 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 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 72 - **Événements en base** : 271 (dont **271 actifs**)
73 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 76 - **Artistes** : 0/271 actifs (0 %)
77 77 - **Image** : 270/271 actifs (100 %)
78 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
80 −- **Syncs enregistrés** (sync_log) : 14
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) : 16
81 81
82 82 ## Erreurs & dépannage
83 83
modified docs/connecteurs/sitq.md +4 −4
@@ -1,6 +1,6 @@
1 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 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 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 82 - **Événements en base** : 1606 (dont **1598 actifs**)
83 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 86 - **Artistes** : 0/1598 actifs (0 %)
87 87 - **Image** : 558/1598 actifs (35 %)
88 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
90 −- **Syncs enregistrés** (sync_log) : 21
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) : 23
91 91
92 92 ## Erreurs & dépannage
93 93
modified docs/connecteurs/terrebonne.md +1 −1
@@ -1,6 +1,6 @@
1 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 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 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 7 ## Description
8 8
9 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
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)
12 −- **Note** : Nécessite TICKETMASTER_API_KEY dans .env (clé gratuite). Sans clé, le connecteur s'annonce « clé manquante » et ne retourne rien.
10 +- **Couverture** : Grands spectacles, sports et concerts — Montréal/Laval/Brossard, Québec, Gatineau
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** : Fonctionne sans clé (scraping découverte). TICKETMASTER_API_KEY optionnelle : active le mode API riche (prix, artistes, images).
13 13
14 14 ## Accès
15 15
16 16 - **Type** : billetterie
17 17 - **Endpoint** (`API`) : `https://app.ticketmaster.com/discovery/v2/events.json`
18 +- **Endpoint** (`DISCOVER`) : `https://www.ticketmaster.ca/discover/{city}?page={page}`
18 19 - **Auth** : Clé API gratuite `TICKETMASTER_API_KEY` (developer.ticketmaster.com)
19 20 - **Pagination** : par numéro de page
20 −- **Backend** : GET direct `requests` (throttlé, UA `SortiKa/1.0` identifié)
21 −- **Throttle** : 0.25 s entre requêtes
22 −- **Accès légal** : API officielle publique avec clé gratuite (developer.ticketmaster.com) — conditions Ticketmaster Developer respectées
21 +- **Backend** : Scrapfly (anti-bot / rendu JS) via `base.get_scrapfly()`
22 +- **Throttle** : 1.0 s entre requêtes
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 25 <details><summary>En-tête documentaire du connecteur</summary>
25 26
26 −> connectors/ticketmaster.py : connecteur Ticketmaster Discovery API (Québec)
27 −> Source : app.ticketmaster.com/discovery/v2/events — API officielle
28 −> publique (clé gratuite sur developer.ticketmaster.com).
29 −> Extraction: GET JSON paginé (countryCode=CA & stateCode=QC, size=200) —
30 −> nom, dates + heure locale, salle géolocalisée (venues lat/lng),
31 −> fourchette de prix (priceRanges min/max), images 16:9,
32 −> classifications (segment/genre) → catégories, attractions →
33 −> artistes, URL de billetterie officielle.
34 −> Accès : API officielle avec clé — quota 5 req/s & 5 000 req/jour
35 −> (throttle 0,25 s ; ~10 requêtes/sync, pagination plafonnée par
36 −> l'API à 1 000 éléments par requête profonde).
37 −> Clé : TICKETMASTER_API_KEY dans .env (voir .env.example). Sans clé,
38 −> le connecteur s'annonce « clé manquante » et ne retourne rien —
39 −> statut « prêt — clé requise » dans data/sources.json.
27 +> connectors/ticketmaster.py : connecteur Ticketmaster Canada (Québec) — SANS clé
28 +> Mode principal (sans clé) : scraping léger des pages découverte publiques
29 +> www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city,
30 +> gatineau). Chaque page embarque son état Next.js (__NEXT_DATA__) : la
31 +> requête serveur `cityEvents` y est déjà résolue avec 20 événements/page
32 +> (titre, date UTC + fuseau, salle géolocalisée lat/lng, adresse, code
33 +> postal, ville, URL billetterie, catégorie majeure, seatmap). Pagination
34 +> par ?page=N (vérifié 2026-08-18 : montreal total=1165, quebec-city=136).
35 +> Aucun prix publié dans ce flux → is_free/price_* inconnus (jamais
36 +> inventés). Artistes : patrons sûrs des titres (artists_from_title).
37 +> robots.txt : /discover/ non interdit (seuls /api/ismds/* le sont).
38 +> Anti-bot : GET direct d'abord ; si bloqué → Scrapfly ASP SANS render_js.
39 +> Plafonds : TM_MAX_EVENTS (défaut 360), TM_PAGES_PER_CITY (défaut 8).
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 45 </details>
42 46
43 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 73 ## Fréquence & budgets
48 74
49 −- **Cadence** : horaire (watcher PM2) dès que la clé est fournie
75 +- **Cadence** : horaire (watcher PM2)
50 76 - **Budgets / plafonds (variables d'environnement)** :
51 77
52 78 | Variable | Défaut | Rôle |
53 79 |---|---|---|
80 +| `TM_MAX_EVENTS` | `360` | — |
81 +| `TM_PAGES_PER_CITY` | `8` | — |
82 +| `SCRAPFLY_API_KEY` | `—` | — |
54 83 | `TICKETMASTER_API_KEY` | `` | — |
55 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 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**)
62 −- **GPS** : 0/0 actifs (—)
63 −- **Prix** : 0/0 actifs (—)
64 −- **Heure (start_time)** : 0/0 actifs (—)
65 −- **Artistes** : 0/0 actifs (—)
66 −- **Image** : 0/0 actifs (—)
67 −- **Salle (venue)** : 0/0 actifs (—)
68 −- **Dernier sync** : 2026-08-18 02:41 — fetched 0, +0 ajoutés, ~0 mis à jour, -0 retirés
69 −- **Syncs enregistrés** (sync_log) : 2
90 +- **Événements en base** : 330 (dont **330 actifs**)
91 +- **GPS** : 330/330 actifs (100 %)
92 +- **Prix** : 0/330 actifs (0 %)
93 +- **Heure (start_time)** : 318/330 actifs (96 %)
94 +- **Artistes** : 0/330 actifs (0 %)
95 +- **Image** : 297/330 actifs (90 %)
96 +- **Salle (venue)** : 330/330 actifs (100 %)
97 +- **Dernier sync** : 2026-08-18 05:51 — fetched 330, +330 ajoutés, ~0 mis à jour, -0 retirés
98 +- **Syncs enregistrés** (sync_log) : 5
70 99
71 100 ## Erreurs & dépannage
72 101
@@ -77,7 +106,7 @@ Aucune erreur dans `sync_log` pour cette source. ✅
77 106
78 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 111 ## Historique
83 112
modified docs/connecteurs/trois-rivieres.md +1 −1
@@ -1,6 +1,6 @@
1 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 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 2 # Sorti-Ka — Agrégateur de sorties & événements (province de Québec)
3 3 # Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 −# connectors/ticketmaster.py : connecteur Ticketmaster Discovery API (Québec)
5 −# Source : app.ticketmaster.com/discovery/v2/events — API officielle
6 −# publique (clé gratuite sur developer.ticketmaster.com).
7 −# Extraction: GET JSON paginé (countryCode=CA & stateCode=QC, size=200) —
8 −# nom, dates + heure locale, salle géolocalisée (venues lat/lng),
9 −# fourchette de prix (priceRanges min/max), images 16:9,
10 −# classifications (segment/genre) → catégories, attractions →
11 −# artistes, URL de billetterie officielle.
12 −# Accès : API officielle avec clé — quota 5 req/s & 5 000 req/jour
13 −# (throttle 0,25 s ; ~10 requêtes/sync, pagination plafonnée par
14 −# l'API à 1 000 éléments par requête profonde).
15 −# Clé : TICKETMASTER_API_KEY dans .env (voir .env.example). Sans clé,
16 −# le connecteur s'annonce « clé manquante » et ne retourne rien —
17 −# statut « prêt — clé requise » dans data/sources.json.
4 +# connectors/ticketmaster.py : connecteur Ticketmaster Canada (Québec) — SANS clé
5 +# Mode principal (sans clé) : scraping léger des pages découverte publiques
6 +# www.ticketmaster.ca/discover/<ville>?page=N (montreal, quebec-city,
7 +# gatineau). Chaque page embarque son état Next.js (__NEXT_DATA__) : la
8 +# requête serveur `cityEvents` y est déjà résolue avec 20 événements/page
9 +# (titre, date UTC + fuseau, salle géolocalisée lat/lng, adresse, code
10 +# postal, ville, URL billetterie, catégorie majeure, seatmap). Pagination
11 +# par ?page=N (vérifié 2026-08-18 : montreal total=1165, quebec-city=136).
12 +# Aucun prix publié dans ce flux → is_free/price_* inconnus (jamais
13 +# inventés). Artistes : patrons sûrs des titres (artists_from_title).
14 +# robots.txt : /discover/ non interdit (seuls /api/ismds/* le sont).
15 +# Anti-bot : GET direct d'abord ; si bloqué → Scrapfly ASP SANS render_js.
16 +# Plafonds : TM_MAX_EVENTS (défaut 360), TM_PAGES_PER_CITY (défaut 8).
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 22 from __future__ import annotations
20 23
24 +import json
21 25 import os
22 26
27 +from ..normalize import artists_from_title, utc_to_local
23 28 from ..schema import Event
24 29 from .base import BaseConnector
25 30
31 +# --- mode API officielle (avec clé) ------------------------------------------
26 32 API = "https://app.ticketmaster.com/discovery/v2/events.json"
27 33 PAGE_SIZE = 200
28 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 53 def _best_image(images: list[dict]) -> str:
32 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 62 class TicketmasterConnector(BaseConnector):
41 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 177 def _page(self, key: str, page: int) -> dict:
45 178 return self.get_json(API, params={
46 179 "apikey": key, "countryCode": "CA", "stateCode": "QC",
@@ -102,13 +235,8 @@ class TicketmasterConnector(BaseConnector):
102 235 image=_best_image(hit.get("images") or []),
103 236 )
104 237
105 − def fetch(self) -> list[Event]:
106 − key = os.environ.get("TICKETMASTER_API_KEY", "").strip()
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 []
238 + def _fetch_api(self, key: str) -> list[Event]:
239 + self.request_delay = 0.25 # quota officiel : 5 req/s
112 240 events: list[Event] = []
113 241 page, total_pages = 0, 1
114 242 while page < total_pages and (page + 1) * PAGE_SIZE <= DEEP_PAGE_LIMIT:
@@ -120,3 +248,10 @@ class TicketmasterConnector(BaseConnector):
120 248 events.append(ev)
121 249 page += 1
122 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 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):
174 − from sortika.connectors.ticketmaster import TicketmasterConnector
173 +def test_ticketmaster_without_key_scrapes_discover(monkeypatch):
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 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 218 def test_ticketmaster_parses_hit():
180 219