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>
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 | |