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