SPB Git forge

spb/auto-ka

Public
61commits 1branches 0releases
14.4 MBsize
maindefault branch
12 days agolast push
Python 61.6% TypeScript 20.9% CSS 11.4% JavaScript 5.1% HTML 1.1%

Vague 3 : grands portails — AutoTrader.ca/AutoHebdo, Otogo.ca, CarGurus.ca

- autotrader : plateforme AutoScout24 (__NEXT_DATA__ des SRP /cars/reg_qc),
  occasion QC triée récent d abord, fenêtre AUTOKA_AUTOTRADER_MAX (déf. 2 000),
  direct + repli Scrapfly ASP sans render_js sur 403 (0 crédit en régime normal)
- otogo : API JSON publique /api-search/ (découverte via bundles Nuxt),
  inventaire complet ~23 700 annonces, VIN ≈99 % + GPS exact du vendeur,
  partition par tranches de prix (profondeur API 1 000/requête),
  AUTOKA_OTOGO_MAX (déf. 0 = tout), 0 crédit externe
- cargurus : searchResults.action JSON via Scrapfly ASP (DataDome),
  VIN 100 % + deal rating/dealScore/expectedPrice dans details,
  cap AUTOKA_CARGURUS_MAX (déf. 500) ≈ 11 crédits/sync
- normalize : nearest_city(lat, lng) — ville la plus proche depuis
  villes_gps.json, nom accentué restauré via sources.json (repli Otogo)
- sources.json : 142 sources (3 portails, autorité dédup type=portail)
- docs connecteurs régénérés (30 fiches) + historique Vague 3

Premier sync : 26 164 annonces ajoutées, dédup VIN 6 655 groupes /
8 438 doublons masqués (otogo→concessions directes : 5 847)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Simon-Pierre Boucher committed 1 mo ago (Aug 18, 2026) parent b6d5459

22 changed files +956 −122

added autoka/connectors/autotrader.py +191 −0
@@ -0,0 +1,191 @@
1 +# -----------------------------------------------------------------------------
2 +# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)
3 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +# connectors/autotrader.py : PORTAIL — AutoTrader.ca / AutoHebdo (Québec)
5 +#
6 +# AutoTrader.ca tourne depuis 2025-2026 sur la plateforme AutoScout24
7 +# (Next.js) : chaque page de résultats (SRP) embarque les 20 annonces
8 +# complètes en JSON dans <script id="__NEXT_DATA__"> — marque/modèle/
9 +# version, année, prix (brut + évaluation de prix), kilométrage, boîte,
10 +# carburant, description, ville/code postal du vendeur, concessionnaire
11 +# vs particulier, téléphone et photos CDN. Pas de VIN dans la liste ni
12 +# sur la page détail (vérifié 2026-08-18) — la dédup VIN ne s'applique
13 +# donc pas à cette source.
14 +#
15 +# URL : /cars/reg_qc?offer=U&sort=age&desc=1&page=N
16 +# - reg_qc : province de Québec seulement
17 +# - offer=U : occasion seulement (34 870 annonces au 2026-08-18)
18 +# - sort=age&desc=1 : plus récentes d'abord — la fenêtre plafonnée
19 +# couvre ainsi les nouveautés à chaque cycle
20 +# - profondeur max : 200 pages × 20 = 4 000 annonces par requête
21 +#
22 +# Plafond : AUTOKA_AUTOTRADER_MAX (défaut 2 000) — fenêtre glissante des
23 +# annonces les plus récentes ; les annonces sorties de la fenêtre passent
24 +# inactives après le délai de grâce standard (MISS_GRACE), la source garde
25 +# donc un volume ≈ plafond en base.
26 +#
27 +# Anti-bot : Akamai présent mais le HTML SSR se sert en direct (vérifié
28 +# pages 1-50) ; repli automatique Scrapfly ASP SANS render_js sur 403/429
29 +# (~1 crédit/page en cas de blocage seulement — budget nul en régime normal).
30 +# -----------------------------------------------------------------------------
31 +from __future__ import annotations
32 +
33 +import json
34 +import os
35 +import re
36 +
37 +import requests
38 +
39 +from ..schema import Vehicle
40 +from .base import BaseConnector
41 +
42 +BASE = "https://www.autotrader.ca"
43 +SRP = BASE + "/cars/reg_qc?offer=U&sort=age&desc=1&page={page}"
44 +
45 +PAGE_SIZE = 20
46 +MAX_PAGES_SITE = 200 # profondeur maximale exposée par la plateforme
47 +
48 +_NEXT_DATA_RE = re.compile(
49 + r'<script id="__NEXT_DATA__" type="application/json">(.*?)</script>', re.S)
50 +
51 +# vignettes liste 250x188 -> photos 640x480 (servies par le CDN AutoScout24)
52 +_IMG_SIZE_RE = re.compile(r"/\d+x\d+\.webp$")
53 +
54 +# libellés d'évaluation de prix AutoScout24 (tracking.priceLabel)
55 +_PRICE_LABELS = {"top-price", "good-price", "fair-price", "expensive",
56 + "too-expensive"}
57 +
58 +
59 +def _max_listings() -> int:
60 + try:
61 + return max(PAGE_SIZE, int(os.environ.get("AUTOKA_AUTOTRADER_MAX",
62 + "2000")))
63 + except ValueError:
64 + return 2000
65 +
66 +
67 +class AutoTraderQuebec(BaseConnector):
68 + """Portail AutoTrader.ca — annonces d'occasion du Québec, récentes d'abord."""
69 +
70 + source_id = "autotrader"
71 + request_delay = 1.5 # politesse : gros portail national
72 +
73 + def __init__(self) -> None:
74 + super().__init__()
75 + self.session.headers.update({
76 + "Accept": "text/html,application/xhtml+xml",
77 + "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.5",
78 + })
79 +
80 + # -- extraction -----------------------------------------------------------
81 +
82 + def _fetch_page(self, page: int) -> tuple[list[dict], int]:
83 + """Annonces embarquées + nombre total de pages d'une page SRP."""
84 + url = SRP.format(page=page)
85 + try:
86 + html = self.get(url).text
87 + except requests.HTTPError as exc: # Akamai : repli ASP économe
88 + status = exc.response.status_code if exc.response is not None else 0
89 + if status not in (403, 429):
90 + raise
91 + html = self.get_scrapfly(url, render_js=False, asp=True)
92 + m = _NEXT_DATA_RE.search(html)
93 + if not m:
94 + raise RuntimeError(f"autotrader : __NEXT_DATA__ introuvable "
95 + f"(page {page})")
96 + pp = json.loads(m.group(1)).get("props", {}).get("pageProps", {})
97 + return (pp.get("listings") or []), int(pp.get("numberOfPages") or 0)
98 +
99 + def _to_vehicle(self, l: dict) -> Vehicle | None:
100 + ext_id = str(l.get("id") or "")
101 + veh_d = l.get("vehicle") or {}
102 + if not ext_id or not l.get("url"):
103 + return None
104 + if (veh_d.get("articleType") or "Car") != "Car":
105 + return None # voitures seulement (atype=C)
106 +
107 + price_d = l.get("price") or {}
108 + price = price_d.get("priceRaw")
109 + price = float(price) if price else None
110 +
111 + loc = l.get("location") or {}
112 + if (loc.get("provinceCode") or "QC") != "QC":
113 + return None # garde-fou : Québec seulement
114 +
115 + seller = l.get("seller") or {}
116 + is_private = (seller.get("type") or "").lower() == "private"
117 + dealer_name = (seller.get("companyName") or "").strip()
118 + if is_private or not dealer_name:
119 + dealer_name = "Particulier (AutoTrader)" if is_private else \
120 + "AutoTrader.ca"
121 +
122 + details: dict = {"forsaleby": "particulier" if is_private
123 + else "concessionnaire"}
124 + label = ((l.get("tracking") or {}).get("priceLabel") or "")
125 + if label in _PRICE_LABELS:
126 + details["autotrader_price_label"] = label
127 + if price_d.get("priceEvaluation") is not None:
128 + details["autotrader_price_evaluation"] = price_d["priceEvaluation"]
129 + zip_code = (loc.get("zip") or "").strip()
130 + if zip_code:
131 + details["postal_code"] = zip_code
132 + phones = seller.get("phones") or []
133 + if phones and phones[0].get("formattedNumber"):
134 + details["dealer_phone"] = phones[0]["formattedNumber"]
135 +
136 + images = [_IMG_SIZE_RE.sub("/640x480.webp", u)
137 + for u in (l.get("images") or []) if u]
138 +
139 + title_bits = [veh_d.get("make") or "", veh_d.get("model") or "",
140 + str(veh_d.get("modelYear") or "")]
141 + return Vehicle(
142 + source=self.source_id,
143 + external_id=ext_id,
144 + url=l["url"],
145 + kind="auto",
146 + title=" ".join(b for b in title_bits if b),
147 + make=veh_d.get("make") or "",
148 + model=veh_d.get("model") or "",
149 + trim=(veh_d.get("modelVersionInput") or "").strip(),
150 + year=veh_d.get("modelYear"),
151 + price=price,
152 + price_label=(price_d.get("priceFormatted") or "").strip(),
153 + mileage_label=veh_d.get("mileageInKm") or "",
154 + transmission=veh_d.get("transmission") or "",
155 + fuel=veh_d.get("fuel") or "",
156 + engine=veh_d.get("motorTypeName") or "",
157 + dealer_name=dealer_name,
158 + city=(loc.get("city") or "").title(),
159 + description=l.get("description") or "",
160 + details=details,
161 + images=images,
162 + )
163 +
164 + # -- contrat ---------------------------------------------------------------
165 +
166 + def fetch(self) -> list[Vehicle]:
167 + cap = _max_listings()
168 + vehicles: dict[str, Vehicle] = {}
169 + listings, n_pages = self._fetch_page(1)
170 + n_pages = min(n_pages or MAX_PAGES_SITE, MAX_PAGES_SITE,
171 + -(-cap // PAGE_SIZE))
172 + for l in listings:
173 + v = self._to_vehicle(l)
174 + if v:
175 + vehicles[v.external_id] = v
176 + for page in range(2, n_pages + 1):
177 + if len(vehicles) >= cap:
178 + break
179 + try:
180 + listings, _ = self._fetch_page(page)
181 + except Exception:
182 + break # fin de pagination / page vide
183 + new = 0
184 + for l in listings:
185 + v = self._to_vehicle(l)
186 + if v and v.external_id not in vehicles:
187 + vehicles[v.external_id] = v
188 + new += 1
189 + if new == 0:
190 + break
191 + return list(vehicles.values())
added autoka/connectors/cargurus.py +172 −0
@@ -0,0 +1,172 @@
1 +# -----------------------------------------------------------------------------
2 +# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)
3 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +# connectors/cargurus.py : PORTAIL — CarGurus.ca (Québec, avec « deal rating »)
5 +#
6 +# CarGurus est protégé par DataDome (403 en direct, page « enable JS »).
7 +# MAIS son API interne de liste répond en JSON pur à travers Scrapfly ASP
8 +# SANS render_js — coût observé : 1 crédit/requête (proxy datacenter) :
9 +# GET /Cars/searchResults.action?zip=H2X1Y4&distance=500
10 +# &entitySelectingHelper.selectedEntity=&offset=N&maxResults=50
11 +# &sortDir=ASC&sortType=AGE_IN_DAYS&filtersModified=true
12 +# → tableau JSON de ~48 annonces ultra-riches : VIN (≈100 % — alimente la
13 +# dédup inter-sources), prix + PRIX ATTENDU + dealRating/dealScore
14 +# (l'évaluation « bonne/mauvaise affaire » maison de CarGurus, stockée
15 +# dans details), km, boîte, carburant, rouage, carrosserie, couleurs,
16 +# moteur, équipements, concessionnaire + ville + téléphone, photo.
17 +#
18 +# Rayon : zip Montréal (H2X 1Y4) + distance=500 km couvre tout le Québec ;
19 +# les débordements ON/NB sont filtrés par sellerRegion == "QC".
20 +# Tri : AGE_IN_DAYS ASC = plus récentes d'abord — fenêtre glissante.
21 +#
22 +# Budget SERRÉ (documenté) : plafond AUTOKA_CARGURUS_MAX (défaut 500)
23 +# → ~11 requêtes ≈ 11 crédits Scrapfly par sync, ~130/jour au rythme
24 +# de 12 syncs/jour. Les annonces sorties de la fenêtre passent inactives
25 +# après le délai de grâce standard (MISS_GRACE).
26 +# -----------------------------------------------------------------------------
27 +from __future__ import annotations
28 +
29 +import json
30 +import os
31 +import re
32 +
33 +from ..schema import Vehicle
34 +from .base import BaseConnector
35 +
36 +API = ("https://www.cargurus.ca/Cars/searchResults.action"
37 + "?zip=H2X1Y4&distance=500&entitySelectingHelper.selectedEntity="
38 + "&offset={offset}&maxResults=50"
39 + "&sortDir=ASC&sortType=AGE_IN_DAYS&filtersModified=true")
40 +
41 +VDP = "https://www.cargurus.ca/Cars/inventorylisting/vdp.action?listingId={id}"
42 +
43 +MAX_OFFSET_SITE = 5000 # garde-fou de pagination
44 +_VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
45 +_DOORS_RE = re.compile(r"(\d+)")
46 +
47 +
48 +def _max_listings() -> int:
49 + try:
50 + return max(48, int(os.environ.get("AUTOKA_CARGURUS_MAX", "500")))
51 + except ValueError:
52 + return 500
53 +
54 +
55 +class CarGurusQuebec(BaseConnector):
56 + """Portail CarGurus.ca — annonces QC récentes + deal rating (via Scrapfly)."""
57 +
58 + source_id = "cargurus"
59 + request_delay = 1.0
60 +
61 + # -- API ---------------------------------------------------------------------
62 +
63 + def _fetch_batch(self, offset: int) -> list[dict]:
64 + """Un lot d'annonces JSON (via Scrapfly ASP, sans render_js).
65 +
66 + L'ASP rate parfois un coup (contenu vide/HTML DataDome) : 2 nouvelles
67 + tentatives avant d'abandonner — coût borné à 3 crédits par lot.
68 + """
69 + import time
70 + last_exc: Exception | None = None
71 + for attempt in range(3):
72 + body = self.get_scrapfly(API.format(offset=offset),
73 + render_js=False, asp=True)
74 + try:
75 + data = json.loads(body)
76 + return data if isinstance(data, list) else []
77 + except ValueError as exc:
78 + last_exc = exc
79 + time.sleep(2.0 * (attempt + 1))
80 + raise RuntimeError(f"cargurus : réponse non-JSON à offset={offset} "
81 + f"({str(last_exc)[:80]})") from last_exc
82 +
83 + def _to_vehicle(self, l: dict) -> Vehicle | None:
84 + ext_id = str(l.get("id") or "")
85 + if not ext_id:
86 + return None
87 + if (l.get("sellerRegion") or "") != "QC":
88 + return None # rayon 500 km : déborde sur ON/NB
89 +
90 + price = l.get("price")
91 + price = float(price) if price else None
92 + mileage = l.get("mileage")
93 + mileage = float(mileage) if mileage is not None else None
94 +
95 + vin = (l.get("vin") or "").strip().upper()
96 + if not _VIN_RE.fullmatch(vin):
97 + vin = ""
98 +
99 + is_private = (l.get("sellerType") or "").upper() == "PRIVATE"
100 + dealer_name = (l.get("serviceProviderName") or "").strip()
101 + if is_private or not dealer_name:
102 + dealer_name = "Particulier (CarGurus)" if is_private else \
103 + "CarGurus.ca"
104 +
105 + details: dict = {"forsaleby": "particulier" if is_private
106 + else "concessionnaire"}
107 + # l'évaluation d'aubaine maison de CarGurus — la valeur ajoutée du portail
108 + for src_key, dst_key in (("dealRating", "cargurus_deal_rating"),
109 + ("dealScore", "cargurus_deal_score"),
110 + ("expectedPrice", "cargurus_expected_price"),
111 + ("priceDifferential",
112 + "cargurus_price_differential"),
113 + ("daysOnMarket", "days_on_market")):
114 + if l.get(src_key) is not None:
115 + details[dst_key] = l[src_key]
116 + if l.get("phoneNumberString"):
117 + details["dealer_phone"] = l["phoneNumberString"]
118 +
119 + doors = None
120 + m = _DOORS_RE.search(l.get("localizedDoors") or "")
121 + if m:
122 + doors = int(m.group(1))
123 +
124 + city = (l.get("sellerCity") or "").split(",")[0].strip()
125 + img = (l.get("originalPictureData") or {}).get("url") or ""
126 +
127 + return Vehicle(
128 + source=self.source_id,
129 + external_id=ext_id,
130 + url=VDP.format(id=ext_id),
131 + kind="auto",
132 + title=l.get("listingTitle") or "",
133 + make=l.get("makeName") or "",
134 + model=l.get("modelName") or "",
135 + trim=(l.get("trimName") or "").strip(),
136 + year=l.get("carYear"),
137 + price=price,
138 + price_label=(l.get("priceString") or "").strip(),
139 + mileage_km=mileage,
140 + transmission=l.get("localizedTransmission") or "",
141 + fuel=l.get("localizedFuelType") or "",
142 + drivetrain=l.get("localizedDriveTrain") or "",
143 + body_type=l.get("bodyTypeName") or "",
144 + exterior_color=l.get("localizedExteriorColor") or "",
145 + interior_color=l.get("localizedInteriorColor") or "",
146 + engine=l.get("localizedEngineDisplayName") or "",
147 + doors=doors,
148 + vin=vin,
149 + stock_number=l.get("stockNumber") or "",
150 + dealer_name=dealer_name,
151 + city=city,
152 + features=[o for o in (l.get("options") or []) if o],
153 + details=details,
154 + images=[img] if img else [],
155 + )
156 +
157 + # -- contrat -----------------------------------------------------------------
158 +
159 + def fetch(self) -> list[Vehicle]:
160 + cap = _max_listings()
161 + vehicles: dict[str, Vehicle] = {}
162 + offset = 0
163 + while len(vehicles) < cap and offset < MAX_OFFSET_SITE:
164 + batch = self._fetch_batch(offset)
165 + if not batch:
166 + break
167 + for l in batch:
168 + v = self._to_vehicle(l)
169 + if v and v.external_id not in vehicles:
170 + vehicles[v.external_id] = v
171 + offset += len(batch)
172 + return list(vehicles.values())
added autoka/connectors/otogo.py +176 −0
@@ -0,0 +1,176 @@
1 +# -----------------------------------------------------------------------------
2 +# Auto-Ka — Agrégateur de voitures usagées à vendre (province de Québec)
3 +# Auteur : Simon-Pierre Boucher — contact@spboucher.ai
4 +# connectors/otogo.py : PORTAIL — Otogo.ca (LC Media, 100 % Québec)
5 +#
6 +# Otogo expose une API JSON publique, sans clé ni rendu JS :
7 +# GET https://www.otogo.ca/api-search/?latlng=0,0&locale=fr&no_facets=
8 +# &rpp=100&page=N&status=Usagé,Certifié,Démo[&price=lo-hi]
9 +# Chaque annonce de la liste est déjà riche : titre/version FR, année,
10 +# prix, kilométrage, VIN (≈100 %), nom du concessionnaire et surtout le
11 +# GPS EXACT du vendeur (latitude/longitude) — parfait pour la recherche
12 +# par rayon et la dédup VIN avec les concessionnaires connectés.
13 +# (Paramètres découverts dans les bundles Nuxt : constantes c.b du module
14 +# 5731047.js — latlng/locale/rpp/page/price/status/no_facets/no_results.)
15 +#
16 +# Profondeur : l'API plafonne à ~1 000 résultats par requête (rpp=100 →
17 +# meta.pages ≤ 10) pour ~24 000 annonces d'occasion. On PARTITIONNE donc
18 +# par tranches de prix (bissection récursive + tranche ouverte « lo- ») :
19 +# chaque annonce n'est téléchargée qu'une fois, ~250 requêtes directes
20 +# par sync complet, zéro crédit d'API de scraping externe.
21 +#
22 +# Ville : absente de la liste — déduite du GPS exact via nearest_city()
23 +# (data/villes_gps.json) pour l'affichage et la région administrative.
24 +# Carburant/boîte : absents de la liste ; repli commun finalize() (jetons
25 +# du titre/de la version). Page détail : /auto/{slug_fr} (non visitée —
26 +# économie ; le VIN et le GPS sont déjà dans la liste).
27 +#
28 +# Plafond : AUTOKA_OTOGO_MAX (défaut 0 = inventaire complet).
29 +# -----------------------------------------------------------------------------
30 +from __future__ import annotations
31 +
32 +import os
33 +import re
34 +
35 +from ..normalize import nearest_city
36 +from ..schema import Vehicle
37 +from .base import BaseConnector
38 +
39 +BASE = "https://www.otogo.ca"
40 +API = BASE + "/api-search/"
41 +
42 +RPP = 100 # résultats par page (max utile observé)
43 +DEPTH = 1000 # profondeur max de pagination par requête
44 +STATUS = "Usagé,Certifié,Démo" # occasion seulement (exclut Neuf)
45 +PRICE_SPLIT_FLOOR = 250 # $ : largeur minimale d'une tranche de prix
46 +
47 +_VIN_RE = re.compile(r"^[A-HJ-NPR-Z0-9]{17}$")
48 +
49 +
50 +def _max_listings() -> int:
51 + try:
52 + return max(0, int(os.environ.get("AUTOKA_OTOGO_MAX", "0")))
53 + except ValueError:
54 + return 0
55 +
56 +
57 +class OtogoQuebec(BaseConnector):
58 + """Portail Otogo.ca — inventaire d'occasion des concessionnaires du Québec."""
59 +
60 + source_id = "otogo"
61 + request_delay = 0.8
62 +
63 + def __init__(self) -> None:
64 + super().__init__()
65 + self.session.headers.update({
66 + "Accept": "application/json",
67 + "Accept-Language": "fr-CA,fr;q=0.9,en;q=0.5",
68 + })
69 +
70 + # -- API --------------------------------------------------------------------
71 +
72 + def _query(self, page: int, lo: int, hi: int | None) -> tuple[dict, list[dict]]:
73 + """Une page de résultats pour la tranche de prix [lo, hi] (hi=None = ouvert)."""
74 + params = {
75 + "latlng": "0,0", "locale": "fr", "no_facets": "",
76 + "rpp": str(RPP), "status": STATUS,
77 + "price": f"{lo}-{'' if hi is None else hi}",
78 + }
79 + if page > 1:
80 + params["page"] = str(page)
81 + data = self.get(API, params=params).json()
82 + meta = data.get("meta") or {}
83 + results = ((data.get("data") or {}).get("results")) or []
84 + return meta, results
85 +
86 + def _to_vehicle(self, r: dict) -> Vehicle | None:
87 + ext_id = str(r.get("id") or "")
88 + slug = r.get("slug_fr") or r.get("slug_en") or ""
89 + if not ext_id or not slug:
90 + return None
91 +
92 + price = r.get("price")
93 + price = float(price) if price and price >= 500 else None
94 + mileage = r.get("mileage")
95 + mileage = float(mileage) if mileage is not None else None
96 +
97 + try:
98 + lat = float(r.get("latitude"))
99 + lng = float(r.get("longitude"))
100 + except (TypeError, ValueError):
101 + lat = lng = None
102 +
103 + vin = (r.get("vin") or "").strip().upper()
104 + if not _VIN_RE.fullmatch(vin):
105 + vin = ""
106 +
107 + details: dict = {}
108 + if r.get("status_fr"):
109 + details["otogo_status"] = r["status_fr"] # Usagé | Certifié | Démo
110 +
111 + return Vehicle(
112 + source=self.source_id,
113 + external_id=ext_id,
114 + url=f"{BASE}/auto/{slug}",
115 + kind="auto",
116 + title=r.get("title_fr") or r.get("title_en") or "",
117 + trim=(r.get("trim_fr") or r.get("trim_en") or "").strip(" |"),
118 + year=r.get("year"),
119 + price=price,
120 + price_label=(f"{price:,.0f} $".replace(",", " ") if price else ""),
121 + mileage_km=mileage,
122 + vin=vin,
123 + dealer_name=(r.get("dealer_name") or "").strip(),
124 + city=nearest_city(lat, lng),
125 + lat=lat,
126 + lng=lng,
127 + details=details,
128 + images=[u for u in [r.get("thumbnail")] if u],
129 + )
130 +
131 + # -- contrat -----------------------------------------------------------------
132 +
133 + def fetch(self) -> list[Vehicle]:
134 + cap = _max_listings()
135 + vehicles: dict[str, Vehicle] = {}
136 + # partition par prix : bissection récursive tant qu'une tranche
137 + # dépasse la profondeur maximale de l'API (~1 000 annonces)
138 + stack: list[tuple[int, int | None]] = [(0, 100_000), (100_000, None)]
139 + while stack:
140 + if cap and len(vehicles) >= cap:
141 + break
142 + lo, hi = stack.pop()
143 + meta, results = self._query(1, lo, hi)
144 + total = int(meta.get("total") or 0)
145 + if total == 0:
146 + continue
147 + if total > DEPTH:
148 + if hi is None: # tranche ouverte : décaler
149 + stack.append((lo + 100_000, None))
150 + stack.append((lo, lo + 100_000))
151 + continue
152 + if hi - lo > PRICE_SPLIT_FLOOR: # bissection
153 + mid = (lo + hi) // 2
154 + stack.append((mid, hi))
155 + stack.append((lo, mid))
156 + continue
157 + # tranche déjà minimale : on prend ce que la profondeur permet
158 + for r in results:
159 + v = self._to_vehicle(r)
160 + if v:
161 + vehicles[v.external_id] = v
162 + pages = int(meta.get("pages") or 1)
163 + for page in range(2, pages + 1):
164 + if cap and len(vehicles) >= cap:
165 + break
166 + try:
167 + _, results = self._query(page, lo, hi)
168 + except Exception:
169 + break # fin de pagination
170 + if not results:
171 + break
172 + for r in results:
173 + v = self._to_vehicle(r)
174 + if v:
175 + vehicles[v.external_id] = v
176 + return list(vehicles.values())
modified autoka/normalize.py +50 −0
@@ -431,3 +431,53 @@ def city_gps(city: str | None) -> tuple[float, float] | None:
431 431 if re.search(rf"(?<![\w-]){re.escape(name)}(?![\w-])", key):
432 432 return table[name]
433 433 return None
434 +
435 +
436 +# ---------------------------------------------------------------------------
437 +# GPS -> ville connue la plus proche (repli pour les sources qui ne
438 +# fournissent QUE les coordonnées du vendeur, ex. le portail Otogo).
439 +# Nom d'affichage : restauré avec accents depuis data/sources.json quand une
440 +# source connue est dans la même ville, sinon clé de villes_gps.json en
441 +# casse de titre.
442 +# ---------------------------------------------------------------------------
443 +
444 +_CITY_DISPLAY: dict[str, str] | None = None
445 +
446 +
447 +def _load_city_display() -> dict[str, str]:
448 + global _CITY_DISPLAY
449 + if _CITY_DISPLAY is None:
450 + import json
451 + from pathlib import Path
452 + table: dict[str, str] = {}
453 + path = Path(__file__).resolve().parent.parent / "data" / "sources.json"
454 + try:
455 + for s in json.loads(path.read_text(encoding="utf-8"))["sources"]:
456 + city = (s.get("city") or "").strip()
457 + if city:
458 + table.setdefault(_gps_key(city), city)
459 + except (OSError, ValueError, KeyError):
460 + pass
461 + _CITY_DISPLAY = table
462 + return _CITY_DISPLAY
463 +
464 +
465 +def nearest_city(lat: float | None, lng: float | None,
466 + max_km: float = 40.0) -> str:
467 + """Ville du Québec (data/villes_gps.json) la plus proche du point donné.
468 +
469 + Approximation équirectangulaire (suffisante à l'échelle du Québec) ;
470 + chaîne vide si aucune ville connue à moins de `max_km` km.
471 + """
472 + if lat is None or lng is None:
473 + return ""
474 + import math
475 + coslat = math.cos(math.radians(lat))
476 + best, best_d2 = "", float("inf")
477 + for name, (clat, clng) in _load_villes_gps().items():
478 + d2 = (clat - lat) ** 2 + ((clng - lng) * coslat) ** 2
479 + if d2 < best_d2:
480 + best, best_d2 = name, d2
481 + if not best or math.sqrt(best_d2) * 111.0 > max_km:
482 + return ""
483 + return _load_city_display().get(best) or best.title()
modified data/sources.json +40 −1
@@ -1557,6 +1557,45 @@
1557 1557 "platform": "Next.js/Apollo — le SRP embarque les AutosListing complets dans __NEXT_DATA__ (attributs canoniques, GPS, VIN parfois) ; kijijiautos.ca (MoVe) n'existe plus (domaine SERVFAIL, 2026-08)",
1558 1558 "connector": "kijiji",
1559 1559 "status": "actif"
1560 + },
1561 + {
1562 + "id": "autotrader",
1563 + "name": "AutoTrader.ca / AutoHebdo (Québec)",
1564 + "url": "https://www.autotrader.ca",
1565 + "listing_url": "https://www.autotrader.ca/cars/reg_qc?offer=U&sort=age&desc=1",
1566 + "city": "",
1567 + "region": "",
1568 + "type": "portail",
1569 + "type_note": "grand portail national (inventaire concessionnaires + quelques particuliers) — fenêtre des plus récentes (AUTOKA_AUTOTRADER_MAX, défaut 2 000) ; pas de VIN exposé",
1570 + "platform": "AutoScout24 / Next.js — SRP __NEXT_DATA__ embarque 20 annonces/page (prix, km, boîte, carburant, ville, photos, priceLabel) ; direct OK, repli Scrapfly ASP sans render_js sur 403",
1571 + "connector": "autotrader",
1572 + "status": "actif"
1573 + },
1574 + {
1575 + "id": "otogo",
1576 + "name": "Otogo.ca",
1577 + "url": "https://www.otogo.ca",
1578 + "listing_url": "https://www.otogo.ca/auto-usagee",
1579 + "city": "",
1580 + "region": "",
1581 + "type": "portail",
1582 + "type_note": "portail QC de LC Media — inventaire complet (~24 000), VIN ≈100 % + GPS exact du vendeur → gros pourvoyeur de dédup inter-sources",
1583 + "platform": "Nuxt / API JSON publique /api-search/ (latlng/locale/rpp/page/price/status) — profondeur 1 000/requête, partition par tranches de prix ; ville déduite du GPS (nearest_city)",
1584 + "connector": "otogo",
1585 + "status": "actif"
1586 + },
1587 + {
1588 + "id": "cargurus",
1589 + "name": "CarGurus.ca (Québec)",
1590 + "url": "https://www.cargurus.ca",
1591 + "listing_url": "https://www.cargurus.ca/Cars/l-Used-Cars-Quebec",
1592 + "city": "",
1593 + "region": "",
1594 + "type": "portail",
1595 + "type_note": "portail national avec deal rating (dealScore/expectedPrice dans details) — cap bas (AUTOKA_CARGURUS_MAX, défaut 500), VIN ≈100 %",
1596 + "platform": "searchResults.action JSON (zip Montréal + distance 500 km, tri AGE_IN_DAYS, filtre sellerRegion=QC) via Scrapfly ASP sans render_js (DataDome) — ~1 crédit/requête, ~11 requêtes/sync",
1597 + "connector": "cargurus",
1598 + "status": "actif"
1560 1599 }
1561 1600 ]
1562 −}
\ No newline at end of file
1601 +}
modified docs/connecteurs/INDEX.md +18 −13
@@ -1,13 +1,13 @@
1 1 # Auto-Ka — Documentation des connecteurs
2 2
3 −_Générée le 2026-08-18 03:24 par `scripts/gen_connector_docs.py` (rejouable : `.venv/bin/python3 scripts/gen_connector_docs.py`)._
3 +_Générée le 2026-08-18 06:00 par `scripts/gen_connector_docs.py` (rejouable : `.venv/bin/python3 scripts/gen_connector_docs.py`)._
4 4
5 5 ## Vue d'ensemble
6 6
7 −- **Sources** : 139 (registre `data/sources.json`)
8 −- **Modules connecteurs** : 27 (`autoka/connectors/`)
9 −- **Annonces en base** : 23110 — dont 20452 actives (19822 autos, 606 motos, 24 scooters)
10 −- **Doublons VIN masqués** : 2315 (voir [dédup VIN](transverse-dedup-vin.md))
7 +- **Sources** : 142 (registre `data/sources.json`)
8 +- **Modules connecteurs** : 30 (`autoka/connectors/`)
9 +- **Annonces en base** : 49397 — dont 46838 actives (46208 autos, 606 motos, 24 scooters)
10 +- **Doublons VIN masqués** : 8438 (voir [dédup VIN](transverse-dedup-vin.md))
11 11 - **Rappels Transports Canada** : 16969 rappels distincts (voir [rappels TC](transverse-rappels-tc.md))
12 12 - **Concessionnaires géolocalisés** : 138 (voir [dealers](transverse-dealers.md))
13 13
@@ -15,30 +15,33 @@ _Générée le 2026-08-18 03:24 par `scripts/gen_connector_docs.py` (rejouable :
15 15
16 16 | Module | Fiche | Sources | Annonces actives | Backend |
17 17 |---|---|---|---|---|
18 −| `d2c_dealers` | [d2c_dealers.md](d2c_dealers.md) | 66 | 5229 | requests direct — Firecrawl par source au besoin |
18 +| `d2c_dealers` | [d2c_dealers.md](d2c_dealers.md) | 66 | 5348 | requests direct — Firecrawl par source au besoin |
19 19 | `sm360_dealers` | [sm360_dealers.md](sm360_dealers.md) | 15 | 1434 | requests direct — Firecrawl par source au besoin |
20 20 | `moto_dealers` | [moto_dealers.md](moto_dealers.md) | 13 | 630 | requests direct |
21 21 | `central_dealers` | [central_dealers.md](central_dealers.md) | 9 | 345 | requests direct |
22 −| `gatsby_dealers` | [gatsby_dealers.md](gatsby_dealers.md) | 6 | 191 | requests direct |
23 −| `magnetis_dealers` | [magnetis_dealers.md](magnetis_dealers.md) | 4 | 423 | requests direct |
22 +| `gatsby_dealers` | [gatsby_dealers.md](gatsby_dealers.md) | 6 | 198 | requests direct |
23 +| `magnetis_dealers` | [magnetis_dealers.md](magnetis_dealers.md) | 4 | 426 | requests direct |
24 24 | `vvu_dealers` | [vvu_dealers.md](vvu_dealers.md) | 4 | 183 | requests direct |
25 25 | `convertus_fc` | [convertus_fc.md](convertus_fc.md) | 3 | 296 | Firecrawl |
26 26 | `albioccasion` | [albioccasion.md](albioccasion.md) | 1 | 1136 | requests direct |
27 27 | `autodurocher` | [autodurocher.md](autodurocher.md) | 1 | 196 | requests direct |
28 28 | `automobileendirect` | [automobileendirect.md](automobileendirect.md) | 1 | 1687 | requests direct |
29 +| `autotrader` | [autotrader.md](autotrader.md) | 1 | 1960 | Scrapfly |
30 +| `cargurus` | [cargurus.md](cargurus.md) | 1 | 504 | Scrapfly |
29 31 | `classeauto` | [classeauto.md](classeauto.md) | 1 | 109 | requests direct |
30 32 | `clubautozone` | [clubautozone.md](clubautozone.md) | 1 | 47 | requests direct |
31 −| `desmeules` | [desmeules.md](desmeules.md) | 1 | 243 | requests direct |
32 −| `dupontford` | [dupontford.md](dupontford.md) | 1 | 65 | requests direct |
33 +| `desmeules` | [desmeules.md](desmeules.md) | 1 | 244 | requests direct |
34 +| `dupontford` | [dupontford.md](dupontford.md) | 1 | 66 | requests direct |
33 35 | `hgregoire` | [hgregoire.md](hgregoire.md) | 1 | 1672 | Scrapfly |
34 36 | `jlkauto` | [jlkauto.md](jlkauto.md) | 1 | 68 | requests direct |
35 −| `kijiji` | [kijiji.md](kijiji.md) | 1 | 3002 | requests direct |
36 −| `leprixdugros` | [leprixdugros.md](leprixdugros.md) | 1 | 886 | Scrapfly |
37 −| `megacentre` | [megacentre.md](megacentre.md) | 1 | 455 | requests direct |
37 +| `kijiji` | [kijiji.md](kijiji.md) | 1 | 3005 | requests direct |
38 +| `leprixdugros` | [leprixdugros.md](leprixdugros.md) | 1 | 973 | Scrapfly |
39 +| `megacentre` | [megacentre.md](megacentre.md) | 1 | 456 | requests direct |
38 40 | `montjolichrysler` | [montjolichrysler.md](montjolichrysler.md) | 1 | 414 | requests direct |
39 41 | `occasionbeaucage` | [occasionbeaucage.md](occasionbeaucage.md) | 1 | 1226 | requests direct |
40 42 | `occasioncharlevoix` | [occasioncharlevoix.md](occasioncharlevoix.md) | 1 | 30 | requests direct |
41 43 | `okaze` | [okaze.md](okaze.md) | 1 | 50 | requests direct |
44 +| `otogo` | [otogo.md](otogo.md) | 1 | 23700 | requests direct |
42 45 | `ototr` | [ototr.md](ototr.md) | 1 | 4 | requests direct |
43 46 | `stefoychrysler` | [stefoychrysler.md](stefoychrysler.md) | 1 | 414 | requests direct |
44 47 | `yannicklaberge` | [yannicklaberge.md](yannicklaberge.md) | 1 | 17 | requests direct |
@@ -58,3 +61,5 @@ _Générée le 2026-08-18 03:24 par `scripts/gen_connector_docs.py` (rejouable :
58 61 |---|---|
59 62 | `2a35ac2` | Enrichissement connecteurs : Carfax, détails AED/LPDG, verticale moto, photos Magnetis, GPS concessionnaires |
60 63 | `bbc070b` | Vague 2 : dédup VIN inter-sources, Kijiji particuliers, rappels Transports Canada, coordonnées concessionnaires |
64 +| `b6d5459` | Documentation standardisée des connecteurs (générateur + fiches) |
65 +| Vague 3 | Grands portails : AutoTrader.ca/AutoHebdo (fenêtre récente), Otogo.ca (inventaire complet, VIN+GPS), CarGurus.ca (deal rating, via Scrapfly) |
added docs/connecteurs/autotrader.md +68 −0
@@ -0,0 +1,68 @@
1 +# Module `autotrader` — AutoScout24 / Next.js — SRP __NEXT_DATA__ embarque 20 annonces/page
2 +
3 +_Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas éditer à la main._
4 +
5 +## Vue d'ensemble
6 +
7 +- **Fichier** : `autoka/connectors/autotrader.py`
8 +- **Sources membres** : 1
9 +- **Annonces actives (BD)** : 1960
10 +- **Backend de fetch** : Scrapfly
11 +- **Mécanique détectée** : __NEXT_DATA__
12 +- **Plafonds / cache** : AUTOKA_AUTOTRADER_MAX
13 +- **Classes** : `AutoTraderQuebec` → `autotrader`
14 +
15 +## Mécanique (en-tête du module)
16 +
17 +> connectors/autotrader.py : PORTAIL — AutoTrader.ca / AutoHebdo (Québec)
18 +>
19 +> AutoTrader.ca tourne depuis 2025-2026 sur la plateforme AutoScout24
20 +> (Next.js) : chaque page de résultats (SRP) embarque les 20 annonces
21 +> complètes en JSON dans <script id="__NEXT_DATA__"> — marque/modèle/
22 +> version, année, prix (brut + évaluation de prix), kilométrage, boîte,
23 +> carburant, description, ville/code postal du vendeur, concessionnaire
24 +> vs particulier, téléphone et photos CDN. Pas de VIN dans la liste ni
25 +> sur la page détail (vérifié 2026-08-18) — la dédup VIN ne s'applique
26 +> donc pas à cette source.
27 +>
28 +> URL : /cars/reg_qc?offer=U&sort=age&desc=1&page=N
29 +> - reg_qc : province de Québec seulement
30 +> - offer=U : occasion seulement (34 870 annonces au 2026-08-18)
31 +> - sort=age&desc=1 : plus récentes d'abord — la fenêtre plafonnée
32 +> couvre ainsi les nouveautés à chaque cycle
33 +> - profondeur max : 200 pages × 20 = 4 000 annonces par requête
34 +>
35 +> Plafond : AUTOKA_AUTOTRADER_MAX (défaut 2 000) — fenêtre glissante des
36 +> annonces les plus récentes ; les annonces sorties de la fenêtre passent
37 +> inactives après le délai de grâce standard (MISS_GRACE), la source garde
38 +> donc un volume ≈ plafond en base.
39 +>
40 +> Anti-bot : Akamai présent mais le HTML SSR se sert en direct (vérifié
41 +> pages 1-50) ; repli automatique Scrapfly ASP SANS render_js sur 403/429
42 +> (~1 crédit/page en cas de blocage seulement — budget nul en régime normal).
43 +
44 +## Complétude des champs (annonces actives, N = 1 960)
45 +
46 +| Champ | % rempli |
47 +|---|---|
48 +| Marque | 100.0 % |
49 +| Modèle | 100.0 % |
50 +| Année | 99.8 % |
51 +| Kilométrage | 99.6 % |
52 +| Prix | 100.0 % |
53 +| Carburant | 95.7 % |
54 +| Boîte (transmission) | 98.6 % |
55 +| VIN | 0.0 % |
56 +| Photos | 97.5 % |
57 +| Carfax | 0.0 % |
58 +| GPS (lat/lng) | 91.2 % |
59 +
60 +## Sources membres (BD live)
61 +
62 +| Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
63 +|---|---|---|---|---|---|---|---|
64 +| `autotrader` | AutoTrader.ca / AutoHebdo (Québec) | — | — | 1960 | 0 | 2026-08-18 05:50 | OK (1960 trouvés) |
65 +
66 +## Erreurs de synchronisation récentes
67 +
68 +Aucune erreur dans le sync_log pour les sources de ce module.
added docs/connecteurs/cargurus.md +64 −0
@@ -0,0 +1,64 @@
1 +# Module `cargurus` — searchResults.action JSON
2 +
3 +_Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas éditer à la main._
4 +
5 +## Vue d'ensemble
6 +
7 +- **Fichier** : `autoka/connectors/cargurus.py`
8 +- **Sources membres** : 1
9 +- **Annonces actives (BD)** : 504
10 +- **Backend de fetch** : Scrapfly
11 +- **Mécanique détectée** : —
12 +- **Plafonds / cache** : AUTOKA_CARGURUS_MAX
13 +- **Classes** : `CarGurusQuebec` → `cargurus`
14 +
15 +## Mécanique (en-tête du module)
16 +
17 +> connectors/cargurus.py : PORTAIL — CarGurus.ca (Québec, avec « deal rating »)
18 +>
19 +> CarGurus est protégé par DataDome (403 en direct, page « enable JS »).
20 +> MAIS son API interne de liste répond en JSON pur à travers Scrapfly ASP
21 +> SANS render_js — coût observé : 1 crédit/requête (proxy datacenter) :
22 +> GET /Cars/searchResults.action?zip=H2X1Y4&distance=500
23 +> &entitySelectingHelper.selectedEntity=&offset=N&maxResults=50
24 +> &sortDir=ASC&sortType=AGE_IN_DAYS&filtersModified=true
25 +> → tableau JSON de ~48 annonces ultra-riches : VIN (≈100 % — alimente la
26 +> dédup inter-sources), prix + PRIX ATTENDU + dealRating/dealScore
27 +> (l'évaluation « bonne/mauvaise affaire » maison de CarGurus, stockée
28 +> dans details), km, boîte, carburant, rouage, carrosserie, couleurs,
29 +> moteur, équipements, concessionnaire + ville + téléphone, photo.
30 +>
31 +> Rayon : zip Montréal (H2X 1Y4) + distance=500 km couvre tout le Québec ;
32 +> les débordements ON/NB sont filtrés par sellerRegion == "QC".
33 +> Tri : AGE_IN_DAYS ASC = plus récentes d'abord — fenêtre glissante.
34 +>
35 +> Budget SERRÉ (documenté) : plafond AUTOKA_CARGURUS_MAX (défaut 500)
36 +> → ~11 requêtes ≈ 11 crédits Scrapfly par sync, ~130/jour au rythme
37 +> de 12 syncs/jour. Les annonces sorties de la fenêtre passent inactives
38 +> après le délai de grâce standard (MISS_GRACE).
39 +
40 +## Complétude des champs (annonces actives, N = 504)
41 +
42 +| Champ | % rempli |
43 +|---|---|
44 +| Marque | 100.0 % |
45 +| Modèle | 100.0 % |
46 +| Année | 99.8 % |
47 +| Kilométrage | 99.6 % |
48 +| Prix | 99.4 % |
49 +| Carburant | 66.1 % |
50 +| Boîte (transmission) | 99.0 % |
51 +| VIN | 99.8 % |
52 +| Photos | 92.3 % |
53 +| Carfax | 0.0 % |
54 +| GPS (lat/lng) | 97.8 % |
55 +
56 +## Sources membres (BD live)
57 +
58 +| Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
59 +|---|---|---|---|---|---|---|---|
60 +| `cargurus` | CarGurus.ca (Québec) | — | — | 504 | 170 | 2026-08-18 05:55 | OK (504 trouvés) |
61 +
62 +## Erreurs de synchronisation récentes
63 +
64 +Aucune erreur dans le sync_log pour les sources de ce module.
modified docs/connecteurs/d2c_dealers.md +56 −56
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/d2c_dealers.py`
8 8 - **Sources membres** : 66
9 −- **Annonces actives (BD)** : 5229
9 +- **Annonces actives (BD)** : 5348
10 10 - **Backend de fetch** : requests direct — Firecrawl par source au besoin (`use_firecrawl`, sites derrière Cloudflare)
11 11 - **Mécanique détectée** : sitemap, JSON-LD schema.org, endpoint JSON
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, cache détail BD (detail_cache)
@@ -40,7 +40,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
40 40 > Le lastmod du sitemap est bumpé quotidiennement par D2C (inutilisable
41 41 > comme clé de cache) — d'où la clé temporelle hebdomadaire.
42 42
43 −## Complétude des champs (annonces actives, N = 5 229)
43 +## Complétude des champs (annonces actives, N = 5 348)
44 44
45 45 | Champ | % rempli |
46 46 |---|---|
@@ -53,7 +53,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
53 53 | Boîte (transmission) | 99.8 % |
54 54 | VIN | 99.3 % |
55 55 | Photos | 99.5 % |
56 −| Carfax | 16.1 % |
56 +| Carfax | 34.5 % |
57 57 | GPS (lat/lng) | 100.0 % |
58 58
59 59 ## Sources membres (BD live)
@@ -61,71 +61,71 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
61 61 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
62 62 |---|---|---|---|---|---|---|---|
63 63 | `abitibiautocredit` | Abitibi Auto Crédit | Rouyn-Noranda | Abitibi-Témiscamingue | 91 | 85 | 2026-08-18 03:05 | OK (89 trouvés) |
64 −| `almahonda` | Alma Honda | Alma | Saguenay–Lac-Saint-Jean | 40 | 0 | 2026-08-17 23:35 | OK (40 trouvés) |
64 +| `almahonda` | Alma Honda | Alma | Saguenay–Lac-Saint-Jean | 41 | 0 | 2026-08-18 05:14 | OK (40 trouvés) |
65 65 | `autoquebeclevis` | AutoQuébec Occasion Lévis | Lévis | Chaudière-Appalaches | 77 | 0 | 2026-08-18 02:59 | OK (76 trouvés) |
66 66 | `autoquebectm` | AutoQuébec Occasion Thetford Mines | Thetford Mines | Chaudière-Appalaches | 35 | 0 | 2026-08-18 03:01 | OK (35 trouvés) |
67 67 | `autosevasion` | Autos Évasion | Sherbrooke | Estrie | 51 | 0 | 2026-08-18 02:46 | OK (50 trouvés) |
68 68 | `autosoccasionld` | Autos Occasion LD | Lévis | Chaudière-Appalaches | 158 | 0 | 2026-08-18 02:53 | OK (158 trouvés) |
69 69 | `autosthubert` | Auto St-Hubert | Saint-Hubert | Montérégie | 143 | 0 | 2026-08-18 03:22 | OK (138 trouvés) |
70 −| `autovilleneuve` | Automobile Villeneuve Amqui | Amqui | Bas-Saint-Laurent | 5 | 0 | 2026-08-17 23:35 | OK (5 trouvés) |
71 −| `berniercrepeau` | Bernier Crépeau | Trois-Rivières | Mauricie | 31 | 0 | 2026-08-17 23:34 | OK (31 trouvés) |
72 −| `charlesbourgtoyota` | Charlesbourg Toyota | Québec | Capitale-Nationale | 97 | 0 | 2026-08-17 23:35 | OK (97 trouvés) |
73 −| `charlevoixtoyota` | Charlevoix Toyota | La Malbaie | Capitale-Nationale | 50 | 0 | 2026-08-17 23:35 | OK (50 trouvés) |
74 −| `chomedeytoyota` | Chomedey Toyota Laval | Laval | Laval | 59 | 0 | 2026-08-17 23:34 | OK (59 trouvés) |
75 −| `drummondvilleford` | Drummondville Ford | Drummondville | Centre-du-Québec | 52 | 0 | 2026-08-17 23:34 | OK (52 trouvés) |
76 −| `drummondvillevw` | Drummondville Volkswagen | Drummondville | Centre-du-Québec | 80 | 0 | 2026-08-17 23:34 | OK (80 trouvés) |
77 −| `dubechrysler` | Dubé Chrysler | Rivière-du-Loup | Bas-Saint-Laurent | 162 | 0 | 2026-08-17 23:34 | OK (162 trouvés) |
78 −| `ducloslongueuil` | Duclos Longueuil Chrysler | Longueuil | Montérégie | 80 | 0 | 2026-08-17 23:34 | OK (80 trouvés) |
79 −| `dupontautomobile` | Dupont Automobile | Alma | Saguenay–Lac-Saint-Jean | 56 | 0 | 2026-08-17 23:35 | OK (56 trouvés) |
80 −| `dynastiehyundai` | Dynastie Hyundai | Rouyn-Noranda | Abitibi-Témiscamingue | 34 | 33 | 2026-08-17 23:34 | OK (34 trouvés) |
70 +| `autovilleneuve` | Automobile Villeneuve Amqui | Amqui | Bas-Saint-Laurent | 5 | 0 | 2026-08-18 05:33 | OK (5 trouvés) |
71 +| `berniercrepeau` | Bernier Crépeau | Trois-Rivières | Mauricie | 31 | 0 | 2026-08-18 03:27 | OK (29 trouvés) |
72 +| `charlesbourgtoyota` | Charlesbourg Toyota | Québec | Capitale-Nationale | 100 | 0 | 2026-08-18 05:25 | OK (99 trouvés) |
73 +| `charlevoixtoyota` | Charlevoix Toyota | La Malbaie | Capitale-Nationale | 53 | 0 | 2026-08-18 05:33 | OK (53 trouvés) |
74 +| `chomedeytoyota` | Chomedey Toyota Laval | Laval | Laval | 68 | 0 | 2026-08-18 03:30 | OK (68 trouvés) |
75 +| `drummondvilleford` | Drummondville Ford | Drummondville | Centre-du-Québec | 55 | 0 | 2026-08-18 03:32 | OK (54 trouvés) |
76 +| `drummondvillevw` | Drummondville Volkswagen | Drummondville | Centre-du-Québec | 95 | 0 | 2026-08-18 03:36 | OK (94 trouvés) |
77 +| `dubechrysler` | Dubé Chrysler | Rivière-du-Loup | Bas-Saint-Laurent | 165 | 0 | 2026-08-18 03:43 | OK (164 trouvés) |
78 +| `ducloslongueuil` | Duclos Longueuil Chrysler | Longueuil | Montérégie | 82 | 0 | 2026-08-18 03:47 | OK (81 trouvés) |
79 +| `dupontautomobile` | Dupont Automobile | Alma | Saguenay–Lac-Saint-Jean | 56 | 0 | 2026-08-18 05:12 | OK (56 trouvés) |
80 +| `dynastiehyundai` | Dynastie Hyundai | Rouyn-Noranda | Abitibi-Témiscamingue | 34 | 33 | 2026-08-18 03:48 | OK (34 trouvés) |
81 81 | `equipeformule` | Équipe Formule | Rimouski | Bas-Saint-Laurent | 71 | 0 | 2026-08-18 02:44 | OK (69 trouvés) |
82 −| `fichaultkia` | Fichault Kia | Châteauguay | Montérégie | 35 | 0 | 2026-08-17 23:34 | OK (35 trouvés) |
83 −| `forceoccasion` | Force Occasion | Québec | Capitale-Nationale | 505 | 0 | 2026-08-18 02:20 | OK (490 trouvés) |
84 −| `fordthetford` | Ford Thetford | Thetford Mines | Chaudière-Appalaches | 33 | 0 | 2026-08-17 23:34 | OK (33 trouvés) |
85 −| `gaspesiehyundai` | Gaspésie Hyundai | Bonaventure | Gaspésie–Îles-de-la-Madeleine | 33 | 30 | 2026-08-17 23:34 | OK (33 trouvés) |
86 −| `gaspetoyota` | Gaspé Toyota | Gaspé | Gaspésie–Îles-de-la-Madeleine | 12 | 0 | 2026-08-17 23:34 | OK (12 trouvés) |
82 +| `fichaultkia` | Fichault Kia | Châteauguay | Montérégie | 37 | 0 | 2026-08-18 03:50 | OK (37 trouvés) |
83 +| `forceoccasion` | Force Occasion | Québec | Capitale-Nationale | 505 | 1 | 2026-08-18 02:20 | OK (490 trouvés) |
84 +| `fordthetford` | Ford Thetford | Thetford Mines | Chaudière-Appalaches | 33 | 0 | 2026-08-18 03:51 | OK (33 trouvés) |
85 +| `gaspesiehyundai` | Gaspésie Hyundai | Bonaventure | Gaspésie–Îles-de-la-Madeleine | 35 | 31 | 2026-08-18 03:53 | OK (34 trouvés) |
86 +| `gaspetoyota` | Gaspé Toyota | Gaspé | Gaspésie–Îles-de-la-Madeleine | 12 | 0 | 2026-08-18 03:51 | OK (12 trouvés) |
87 87 | `gervaisauto` | Gervais Auto | Shawinigan | Mauricie | 559 | 0 | 2026-08-18 02:41 | OK (548 trouvés) |
88 88 | `girardautos` | Girard Autos | Trois-Rivières | Mauricie | 20 | 0 | 2026-08-18 02:17 | OK (20 trouvés) |
89 −| `granbytoyota` | Granby Toyota | Granby | Montérégie | 20 | 0 | 2026-08-17 23:34 | OK (20 trouvés) |
90 −| `grandportage` | Groupe Grand-Portage | Rivière-du-Loup | Bas-Saint-Laurent | 112 | 0 | 2026-08-17 23:34 | OK (112 trouvés) |
91 −| `groupemotion` | Groupe Motion | Longueuil | Montérégie | 69 | 0 | 2026-08-17 23:34 | OK (69 trouvés) |
92 −| `groupepaquin` | Groupe Paquin | Rouyn-Noranda | Abitibi-Témiscamingue | 50 | 0 | 2026-08-17 23:34 | OK (50 trouvés) |
93 −| `hondacharlesbourg` | Honda Charlesbourg | Québec | Capitale-Nationale | 14 | 0 | 2026-08-17 23:35 | OK (14 trouvés) |
94 −| `hondacharlevoix` | Honda Charlevoix | La Malbaie | Capitale-Nationale | 35 | 0 | 2026-08-17 23:35 | OK (35 trouvés) |
95 −| `hondanewrichmond` | Honda New Richmond | New Richmond | Gaspésie–Îles-de-la-Madeleine | 60 | 0 | 2026-08-17 23:35 | OK (60 trouvés) |
96 −| `hullhyundai` | Hull Hyundai | Gatineau | Outaouais | 97 | 0 | 2026-08-17 23:34 | OK (97 trouvés) |
97 −| `hyundaibeauce` | Hyundai Beauce | Saint-Georges | Chaudière-Appalaches | 31 | 0 | 2026-08-17 23:35 | OK (31 trouvés) |
98 −| `hyundaicharlevoix` | Hyundai Charlevoix | Baie-Saint-Paul | Capitale-Nationale | 22 | 0 | 2026-08-17 23:35 | OK (22 trouvés) |
99 −| `hyundaidrummondville` | Hyundai Drummondville | Drummondville | Centre-du-Québec | 89 | 0 | 2026-08-17 23:35 | OK (89 trouvés) |
100 −| `hyundailanaudiere` | Hyundai Lanaudière | Joliette | Lanaudière | 60 | 0 | 2026-08-17 23:35 | OK (60 trouvés) |
101 −| `hyundailongueuil` | Hyundai Longueuil | Longueuil | Montérégie | 16 | 0 | 2026-08-17 23:35 | OK (16 trouvés) |
102 −| `hyundaistjerome` | Hyundai St-Jérôme | Mirabel | Laurentides | 115 | 0 | 2026-08-17 23:35 | OK (115 trouvés) |
103 −| `jeandumas` | Jean Dumas Multiconcessionnaire | Alma | Saguenay–Lac-Saint-Jean | 105 | 105 | 2026-08-17 23:35 | OK (105 trouvés) |
104 −| `jeandumasfordbc` | Jean Dumas Ford Baie-Comeau | Baie-Comeau | Côte-Nord | 105 | 0 | 2026-08-17 23:35 | OK (105 trouvés) |
105 −| `jeandumashondabc` | Jean Dumas Honda Baie-Comeau | Baie-Comeau | Côte-Nord | 105 | 105 | 2026-08-17 23:35 | OK (105 trouvés) |
106 −| `joliettemitsubishi` | Joliette Mitsubishi | Joliette | Lanaudière | 68 | 0 | 2026-08-17 23:35 | OK (68 trouvés) |
107 −| `kiabeauport` | Kia Beauport | Québec | Capitale-Nationale | 72 | 0 | 2026-08-17 23:35 | OK (72 trouvés) |
108 −| `kiajoliette` | Kia Joliette | Joliette | Lanaudière | 58 | 0 | 2026-08-17 23:35 | OK (58 trouvés) |
109 −| `lamihonda` | L'Ami Honda | Rouyn-Noranda | Abitibi-Témiscamingue | 91 | 0 | 2026-08-17 23:35 | OK (91 trouvés) |
110 −| `leoautomobile` | Léo Automobile | Chicoutimi | Saguenay–Lac-Saint-Jean | 62 | 0 | 2026-08-17 23:35 | OK (62 trouvés) |
89 +| `granbytoyota` | Granby Toyota | Granby | Montérégie | 21 | 0 | 2026-08-18 03:54 | OK (20 trouvés) |
90 +| `grandportage` | Groupe Grand-Portage | Rivière-du-Loup | Bas-Saint-Laurent | 114 | 0 | 2026-08-18 03:58 | OK (108 trouvés) |
91 +| `groupemotion` | Groupe Motion | Longueuil | Montérégie | 73 | 0 | 2026-08-18 04:01 | OK (71 trouvés) |
92 +| `groupepaquin` | Groupe Paquin | Rouyn-Noranda | Abitibi-Témiscamingue | 50 | 0 | 2026-08-18 04:03 | OK (50 trouvés) |
93 +| `hondacharlesbourg` | Honda Charlesbourg | Québec | Capitale-Nationale | 15 | 0 | 2026-08-18 05:25 | OK (14 trouvés) |
94 +| `hondacharlevoix` | Honda Charlevoix | La Malbaie | Capitale-Nationale | 36 | 0 | 2026-08-18 05:30 | OK (36 trouvés) |
95 +| `hondanewrichmond` | Honda New Richmond | New Richmond | Gaspésie–Îles-de-la-Madeleine | 60 | 0 | 2026-08-18 05:16 | OK (60 trouvés) |
96 +| `hullhyundai` | Hull Hyundai | Gatineau | Outaouais | 102 | 0 | 2026-08-18 04:07 | OK (98 trouvés) |
97 +| `hyundaibeauce` | Hyundai Beauce | Saint-Georges | Chaudière-Appalaches | 32 | 0 | 2026-08-18 04:08 | OK (31 trouvés) |
98 +| `hyundaicharlevoix` | Hyundai Charlevoix | Baie-Saint-Paul | Capitale-Nationale | 22 | 0 | 2026-08-18 05:31 | OK (21 trouvés) |
99 +| `hyundaidrummondville` | Hyundai Drummondville | Drummondville | Centre-du-Québec | 92 | 0 | 2026-08-18 04:12 | OK (88 trouvés) |
100 +| `hyundailanaudiere` | Hyundai Lanaudière | Joliette | Lanaudière | 60 | 0 | 2026-08-18 04:15 | OK (60 trouvés) |
101 +| `hyundailongueuil` | Hyundai Longueuil | Longueuil | Montérégie | 17 | 0 | 2026-08-18 04:15 | OK (17 trouvés) |
102 +| `hyundaistjerome` | Hyundai St-Jérôme | Mirabel | Laurentides | 122 | 0 | 2026-08-18 04:20 | OK (114 trouvés) |
103 +| `jeandumas` | Jean Dumas Multiconcessionnaire | Alma | Saguenay–Lac-Saint-Jean | 108 | 108 | 2026-08-18 04:24 | OK (100 trouvés) |
104 +| `jeandumasfordbc` | Jean Dumas Ford Baie-Comeau | Baie-Comeau | Côte-Nord | 108 | 76 | 2026-08-18 04:28 | OK (100 trouvés) |
105 +| `jeandumashondabc` | Jean Dumas Honda Baie-Comeau | Baie-Comeau | Côte-Nord | 108 | 32 | 2026-08-18 04:33 | OK (100 trouvés) |
106 +| `joliettemitsubishi` | Joliette Mitsubishi | Joliette | Lanaudière | 72 | 0 | 2026-08-18 04:35 | OK (72 trouvés) |
107 +| `kiabeauport` | Kia Beauport | Québec | Capitale-Nationale | 72 | 0 | 2026-08-18 05:28 | OK (72 trouvés) |
108 +| `kiajoliette` | Kia Joliette | Joliette | Lanaudière | 60 | 0 | 2026-08-18 04:38 | OK (54 trouvés) |
109 +| `lamihonda` | L'Ami Honda | Rouyn-Noranda | Abitibi-Témiscamingue | 92 | 0 | 2026-08-18 04:42 | OK (90 trouvés) |
110 +| `leoautomobile` | Léo Automobile | Chicoutimi | Saguenay–Lac-Saint-Jean | 67 | 0 | 2026-08-18 04:44 | OK (64 trouvés) |
111 111 | `levisautos` | Lévis Autos | Lévis | Chaudière-Appalaches | 91 | 0 | 2026-08-18 02:56 | OK (83 trouvés) |
112 −| `levistoyota` | Lévis Toyota | Lévis | Chaudière-Appalaches | 45 | 0 | 2026-08-17 23:35 | OK (45 trouvés) |
113 −| `longueuilhonda` | Longueuil Honda | Longueuil | Montérégie | 44 | 0 | 2026-08-17 23:35 | OK (44 trouvés) |
112 +| `levistoyota` | Lévis Toyota | Lévis | Chaudière-Appalaches | 45 | 0 | 2026-08-18 04:46 | OK (45 trouvés) |
113 +| `longueuilhonda` | Longueuil Honda | Longueuil | Montérégie | 49 | 0 | 2026-08-18 04:48 | OK (49 trouvés) |
114 114 | `mferlandauto` | M. Ferland Auto | Sherbrooke | Estrie | 14 | 0 | 2026-08-18 02:47 | OK (13 trouvés) |
115 115 | `nicoloccasion` | Nicol Occasion | La Sarre | Abitibi-Témiscamingue | 57 | 0 | 2026-08-18 03:07 | OK (57 trouvés) |
116 −| `olivierchryslerbc` | Olivier Chrysler Baie-Comeau | Baie-Comeau | Côte-Nord | 39 | 0 | 2026-08-17 23:35 | OK (39 trouvés) |
117 −| `olivierhyundaibc` | Olivier Hyundai Baie-Comeau | Baie-Comeau | Côte-Nord | 20 | 0 | 2026-08-17 23:35 | OK (20 trouvés) |
118 −| `olivieroccasionseptiles` | Olivier Occasion Sept-Îles | Sept-Îles | Côte-Nord | 88 | 0 | 2026-08-17 23:35 | OK (88 trouvés) |
116 +| `olivierchryslerbc` | Olivier Chrysler Baie-Comeau | Baie-Comeau | Côte-Nord | 40 | 0 | 2026-08-18 04:50 | OK (37 trouvés) |
117 +| `olivierhyundaibc` | Olivier Hyundai Baie-Comeau | Baie-Comeau | Côte-Nord | 20 | 0 | 2026-08-18 05:17 | OK (20 trouvés) |
118 +| `olivieroccasionseptiles` | Olivier Occasion Sept-Îles | Sept-Îles | Côte-Nord | 89 | 0 | 2026-08-18 05:21 | OK (86 trouvés) |
119 119 | `poirieroccasion` | Poirier Occasion | Val-d'Or | Abitibi-Témiscamingue | 231 | 0 | 2026-08-18 03:16 | OK (230 trouvés) |
120 −| `sherbrookeautooccasion` | Sherbrooke Auto Occasion | Sherbrooke | Estrie | 91 | 0 | 2026-08-17 23:34 | OK (91 trouvés) |
121 −| `stgeorgesgm` | St-Georges Chevrolet Buick GMC | Saint-Georges | Chaudière-Appalaches | 29 | 0 | 2026-08-17 23:35 | OK (29 trouvés) |
122 −| `stjeromemitsubishi` | St-Jérôme Mitsubishi | Saint-Jérôme | Laurentides | 129 | 0 | 2026-08-17 23:35 | OK (129 trouvés) |
123 −| `subaruvictoriaville` | Subaru Victoriaville | Victoriaville | Centre-du-Québec | 43 | 0 | 2026-08-17 23:35 | OK (43 trouvés) |
124 −| `toyotabdc` | Toyota Baie des Chaleurs | Caplan | Gaspésie–Îles-de-la-Madeleine | 39 | 3 | 2026-08-17 23:35 | OK (39 trouvés) |
125 −| `troisriviereshonda` | Trois-Rivières Honda | Trois-Rivières | Mauricie | 76 | 0 | 2026-08-17 23:35 | OK (76 trouvés) |
126 −| `trtoyota` | Trois-Rivières Toyota | Trois-Rivières | Mauricie | 58 | 0 | 2026-08-17 23:35 | OK (58 trouvés) |
127 −| `vaudreuilvw` | Vaudreuil Volkswagen | Vaudreuil-Dorion | Montérégie | 40 | 0 | 2026-08-17 23:35 | OK (40 trouvés) |
128 −| `vwsherbrooke` | VW Sherbrooke | Sherbrooke | Estrie | 70 | 0 | 2026-08-17 23:35 | OK (70 trouvés) |
120 +| `sherbrookeautooccasion` | Sherbrooke Auto Occasion | Sherbrooke | Estrie | 91 | 0 | 2026-08-18 03:26 | OK (90 trouvés) |
121 +| `stgeorgesgm` | St-Georges Chevrolet Buick GMC | Saint-Georges | Chaudière-Appalaches | 32 | 0 | 2026-08-18 04:51 | OK (32 trouvés) |
122 +| `stjeromemitsubishi` | St-Jérôme Mitsubishi | Saint-Jérôme | Laurentides | 136 | 0 | 2026-08-18 04:56 | OK (129 trouvés) |
123 +| `subaruvictoriaville` | Subaru Victoriaville | Victoriaville | Centre-du-Québec | 44 | 0 | 2026-08-18 04:58 | OK (42 trouvés) |
124 +| `toyotabdc` | Toyota Baie des Chaleurs | Caplan | Gaspésie–Îles-de-la-Madeleine | 41 | 4 | 2026-08-18 05:00 | OK (35 trouvés) |
125 +| `troisriviereshonda` | Trois-Rivières Honda | Trois-Rivières | Mauricie | 80 | 0 | 2026-08-18 05:03 | OK (74 trouvés) |
126 +| `trtoyota` | Trois-Rivières Toyota | Trois-Rivières | Mauricie | 61 | 0 | 2026-08-18 05:05 | OK (57 trouvés) |
127 +| `vaudreuilvw` | Vaudreuil Volkswagen | Vaudreuil-Dorion | Montérégie | 42 | 0 | 2026-08-18 05:07 | OK (42 trouvés) |
128 +| `vwsherbrooke` | VW Sherbrooke | Sherbrooke | Estrie | 70 | 0 | 2026-08-18 05:10 | OK (70 trouvés) |
129 129
130 130 ## Erreurs de synchronisation récentes (sync_log, ok = 0)
131 131
modified docs/connecteurs/desmeules.md +4 −4
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/desmeules.py`
8 8 - **Sources membres** : 1
9 −- **Annonces actives (BD)** : 243
9 +- **Annonces actives (BD)** : 244
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : flux RSS, microdata schema.org
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, cache détail BD (detail_cache)
@@ -35,7 +35,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
35 35 > changent pas ; le prix frais vient du RSS). AUTOKA_MAX_DETAILS (env)
36 36 > plafonne les vraies requêtes détail par synchronisation.
37 37
38 −## Complétude des champs (annonces actives, N = 243)
38 +## Complétude des champs (annonces actives, N = 244)
39 39
40 40 | Champ | % rempli |
41 41 |---|---|
@@ -44,7 +44,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
44 44 | Année | 100.0 % |
45 45 | Kilométrage | 100.0 % |
46 46 | Prix | 98.4 % |
47 −| Carburant | 83.1 % |
47 +| Carburant | 83.2 % |
48 48 | Boîte (transmission) | 98.4 % |
49 49 | VIN | 99.6 % |
50 50 | Photos | 100.0 % |
@@ -55,7 +55,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
55 55
56 56 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
57 57 |---|---|---|---|---|---|---|---|
58 −| `desmeules` | Desmeules Chrysler | Laval | Laval | 243 | 0 | 2026-08-17 23:35 | OK (243 trouvés) |
58 +| `desmeules` | Desmeules Chrysler | Laval | Laval | 244 | 0 | 2026-08-18 05:33 | OK (235 trouvés) |
59 59
60 60 ## Erreurs de synchronisation récentes
61 61
modified docs/connecteurs/dupontford.md +4 −4
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/dupontford.py`
8 8 - **Sources membres** : 1
9 −- **Annonces actives (BD)** : 65
9 +- **Annonces actives (BD)** : 66
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : sitemap, endpoint JSON
12 12 - **Plafonds / cache** : cache détail BD (detail_cache)
@@ -28,7 +28,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
28 28 > URL détail reconstruite comme dans le sitemap inventory.xml :
29 29 > /vue/used-<année>-<marque>-<modèle>-<id>/.
30 30
31 −## Complétude des champs (annonces actives, N = 65)
31 +## Complétude des champs (annonces actives, N = 66)
32 32
33 33 | Champ | % rempli |
34 34 |---|---|
@@ -40,7 +40,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
40 40 | Carburant | 100.0 % |
41 41 | Boîte (transmission) | 100.0 % |
42 42 | VIN | 100.0 % |
43 −| Photos | 92.3 % |
43 +| Photos | 92.4 % |
44 44 | Carfax | 0.0 % |
45 45 | GPS (lat/lng) | 100.0 % |
46 46
@@ -48,7 +48,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
48 48
49 49 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
50 50 |---|---|---|---|---|---|---|---|
51 −| `dupontdupontford` | Dupont & Dupont Ford | Gatineau | Outaouais | 65 | 0 | 2026-08-17 23:35 | OK (65 trouvés) |
51 +| `dupontdupontford` | Dupont & Dupont Ford | Gatineau | Outaouais | 66 | 0 | 2026-08-18 05:33 | OK (65 trouvés) |
52 52
53 53 ## Erreurs de synchronisation récentes
54 54
modified docs/connecteurs/gatsby_dealers.md +9 −9
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/gatsby_dealers.py`
8 8 - **Sources membres** : 6
9 −- **Annonces actives (BD)** : 191
9 +- **Annonces actives (BD)** : 198
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : sitemap, GraphQL, AWS AppSync, page-data Gatsby, endpoint JSON
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, AUTOKA_MAX_PAGES
@@ -34,7 +34,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
34 34 > usedCar -> Vehicle) est hérité de MegaCentreDeLiquidation.
35 35 > AUTOKA_MAX_PAGES (env) limite la pagination AppSync pour les tests.
36 36
37 −## Complétude des champs (annonces actives, N = 191)
37 +## Complétude des champs (annonces actives, N = 198)
38 38
39 39 | Champ | % rempli |
40 40 |---|---|
@@ -47,19 +47,19 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
47 47 | Boîte (transmission) | 100.0 % |
48 48 | VIN | 100.0 % |
49 49 | Photos | 100.0 % |
50 −| Carfax | 51.3 % |
50 +| Carfax | 52.0 % |
51 51 | GPS (lat/lng) | 100.0 % |
52 52
53 53 ## Sources membres (BD live)
54 54
55 55 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
56 56 |---|---|---|---|---|---|---|---|
57 −| `kiadrummondville` | Kia Drummondville | Drummondville | Centre-du-Québec | 22 | 0 | 2026-08-17 23:35 | OK (22 trouvés) |
58 −| `kiagranby` | Kia de Granby | Granby | Montérégie | 19 | 0 | 2026-08-17 23:35 | OK (19 trouvés) |
59 −| `mazdadesherbrooke` | Mazda de Sherbrooke | Sherbrooke | Estrie | 49 | 0 | 2026-08-17 23:35 | OK (49 trouvés) |
60 −| `mitsubishidrummondville` | Mitsubishi Drummondville | Drummondville | Centre-du-Québec | 14 | 0 | 2026-08-17 23:35 | OK (14 trouvés) |
61 −| `nissandrummondville` | Nissan Drummondville | Drummondville | Centre-du-Québec | 44 | 0 | 2026-08-17 23:35 | OK (44 trouvés) |
62 −| `nissanvictoriaville` | Nissan de Victoriaville | Victoriaville | Centre-du-Québec | 43 | 0 | 2026-08-17 23:35 | OK (43 trouvés) |
57 +| `kiadrummondville` | Kia Drummondville | Drummondville | Centre-du-Québec | 22 | 0 | 2026-08-18 05:33 | OK (22 trouvés) |
58 +| `kiagranby` | Kia de Granby | Granby | Montérégie | 19 | 0 | 2026-08-18 05:33 | OK (19 trouvés) |
59 +| `mazdadesherbrooke` | Mazda de Sherbrooke | Sherbrooke | Estrie | 54 | 0 | 2026-08-18 05:33 | OK (54 trouvés) |
60 +| `mitsubishidrummondville` | Mitsubishi Drummondville | Drummondville | Centre-du-Québec | 14 | 0 | 2026-08-18 05:33 | OK (14 trouvés) |
61 +| `nissandrummondville` | Nissan Drummondville | Drummondville | Centre-du-Québec | 44 | 0 | 2026-08-18 05:33 | OK (44 trouvés) |
62 +| `nissanvictoriaville` | Nissan de Victoriaville | Victoriaville | Centre-du-Québec | 45 | 0 | 2026-08-18 05:33 | OK (45 trouvés) |
63 63
64 64 ## Erreurs de synchronisation récentes
65 65
modified docs/connecteurs/hgregoire.md +1 −1
@@ -65,7 +65,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
65 65
66 66 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
67 67 |---|---|---|---|---|---|---|---|
68 −| `hgregoire` | HGrégoire | St-Léonard | Montréal | 1672 | 2 | 2026-08-17 23:48 | OK (1672 trouvés) |
68 +| `hgregoire` | HGrégoire | St-Léonard | Montréal | 1672 | 2 | 2026-08-18 05:46 | OK (1672 trouvés) |
69 69
70 70 ## Erreurs de synchronisation récentes
71 71
modified docs/connecteurs/jlkauto.md +1 −1
@@ -43,7 +43,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
43 43
44 44 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
45 45 |---|---|---|---|---|---|---|---|
46 −| `jlkauto` | JLK Auto | Montréal | Montréal | 68 | 0 | 2026-08-17 23:48 | OK (68 trouvés) |
46 +| `jlkauto` | JLK Auto | Montréal | Montréal | 68 | 0 | 2026-08-18 05:46 | OK (68 trouvés) |
47 47
48 48 ## Erreurs de synchronisation récentes
49 49
modified docs/connecteurs/kijiji.md +3 −3
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/kijiji.py`
8 8 - **Sources membres** : 1
9 −- **Annonces actives (BD)** : 3002
9 +- **Annonces actives (BD)** : 3005
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : __NEXT_DATA__
12 12 - **Plafonds / cache** : aucun
@@ -34,7 +34,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
34 34 > Pagination : /b-autos-camions/quebec/page-N/c174l9001?for-sale-by=ownr
35 35 > jusqu'à totalCount (bornée par MAX_PAGES par politesse).
36 36
37 −## Complétude des champs (annonces actives, N = 3 002)
37 +## Complétude des champs (annonces actives, N = 3 005)
38 38
39 39 | Champ | % rempli |
40 40 |---|---|
@@ -54,7 +54,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
54 54
55 55 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
56 56 |---|---|---|---|---|---|---|---|
57 −| `kijiji` | Kijiji (particuliers) | — | — | 3002 | 10 | 2026-08-18 02:09 | OK (3002 trouvés) |
57 +| `kijiji` | Kijiji (particuliers) | — | — | 3005 | 14 | 2026-08-18 05:49 | OK (3001 trouvés) |
58 58
59 59 ## Erreurs de synchronisation récentes
60 60
modified docs/connecteurs/leprixdugros.md +5 −5
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/leprixdugros.py`
8 8 - **Sources membres** : 1
9 −- **Annonces actives (BD)** : 886
9 +- **Annonces actives (BD)** : 973
10 10 - **Backend de fetch** : Scrapfly
11 11 - **Mécanique détectée** : endpoint JSON
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, AUTOKA_MAX_PAGES, cache détail BD (detail_cache)
@@ -42,7 +42,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
42 42 > (hybride/EV/diesel), un moteur « N cylindres X.XL » sans indice électrifié
43 43 > => Essence (sauf modèles hybrides « sans badge » connus, ex. Prius).
44 44
45 −## Complétude des champs (annonces actives, N = 886)
45 +## Complétude des champs (annonces actives, N = 973)
46 46
47 47 | Champ | % rempli |
48 48 |---|---|
@@ -51,18 +51,18 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
51 51 | Année | 100.0 % |
52 52 | Kilométrage | 100.0 % |
53 53 | Prix | 99.9 % |
54 −| Carburant | 97.4 % |
54 +| Carburant | 97.6 % |
55 55 | Boîte (transmission) | 100.0 % |
56 56 | VIN | 100.0 % |
57 57 | Photos | 100.0 % |
58 58 | Carfax | 0.0 % |
59 −| GPS (lat/lng) | 42.0 % |
59 +| GPS (lat/lng) | 38.4 % |
60 60
61 61 ## Sources membres (BD live)
62 62
63 63 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
64 64 |---|---|---|---|---|---|---|---|
65 −| `leprixdugros` | Le Prix du Gros | Trois-Rivières | Mauricie | 886 | 653 | 2026-08-18 01:44 | OK (107 trouvés) |
65 +| `leprixdugros` | Le Prix du Gros | Trois-Rivières | Mauricie | 973 | 738 | 2026-08-18 05:50 | OK (654 trouvés) |
66 66
67 67 ## Erreurs de synchronisation récentes (sync_log, ok = 0)
68 68
modified docs/connecteurs/magnetis_dealers.md +9 −9
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/magnetis_dealers.py`
8 8 - **Sources membres** : 4
9 −- **Annonces actives (BD)** : 423
9 +- **Annonces actives (BD)** : 426
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : sitemap, JSON-LD schema.org, blob JSON data-class
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, cache détail BD (detail_cache)
@@ -32,19 +32,19 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
32 32 > Pages détail en cache BD (clé hebdomadaire) ; AUTOKA_MAX_DETAILS (env)
33 33 > plafonne les vraies requêtes détail par synchronisation.
34 34
35 −## Complétude des champs (annonces actives, N = 423)
35 +## Complétude des champs (annonces actives, N = 426)
36 36
37 37 | Champ | % rempli |
38 38 |---|---|
39 39 | Marque | 100.0 % |
40 40 | Modèle | 100.0 % |
41 41 | Année | 99.8 % |
42 −| Kilométrage | 98.3 % |
42 +| Kilométrage | 98.4 % |
43 43 | Prix | 100.0 % |
44 −| Carburant | 94.3 % |
45 −| Boîte (transmission) | 88.2 % |
46 −| VIN | 2.4 % |
47 −| Photos | 2.1 % |
44 +| Carburant | 94.6 % |
45 +| Boîte (transmission) | 88.7 % |
46 +| VIN | 73.2 % |
47 +| Photos | 66.4 % |
48 48 | Carfax | 0.0 % |
49 49 | GPS (lat/lng) | 100.0 % |
50 50
@@ -52,9 +52,9 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
52 52
53 53 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
54 54 |---|---|---|---|---|---|---|---|
55 −| `baiecomeautoyota` | Baie-Comeau Toyota | Baie-Comeau | Côte-Nord | 247 | 0 | 2026-08-18 01:43 | OK (10 trouvés) |
55 +| `baiecomeautoyota` | Baie-Comeau Toyota | Baie-Comeau | Côte-Nord | 250 | 0 | 2026-08-18 05:57 | OK (249 trouvés) |
56 56 | `lussierchevrolet` | Lussier Chevrolet Buick GMC | Saint-Hyacinthe | Montérégie | 83 | 0 | 2026-08-17 23:49 | OK (83 trouvés) |
57 −| `sherbrooketoyota` | Sherbrooke Toyota | Sherbrooke | Estrie | 63 | 0 | 2026-08-17 23:49 | OK (63 trouvés) |
57 +| `sherbrooketoyota` | Sherbrooke Toyota | Sherbrooke | Estrie | 63 | 0 | 2026-08-18 06:00 | OK (63 trouvés) |
58 58 | `stgnissan` | St-Georges Nissan | Saint-Georges | Chaudière-Appalaches | 30 | 0 | 2026-08-17 23:49 | OK (30 trouvés) |
59 59
60 60 ## Erreurs de synchronisation récentes
modified docs/connecteurs/megacentre.md +4 −4
@@ -6,7 +6,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
6 6
7 7 - **Fichier** : `autoka/connectors/megacentre.py`
8 8 - **Sources membres** : 1
9 −- **Annonces actives (BD)** : 455
9 +- **Annonces actives (BD)** : 456
10 10 - **Backend de fetch** : requests direct (aucun anti-bot)
11 11 - **Mécanique détectée** : sitemap, page-data Gatsby, endpoint JSON
12 12 - **Plafonds / cache** : AUTOKA_MAX_DETAILS, cache détail BD (detail_cache)
@@ -33,7 +33,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
33 33 > hebdomadaire génèrent de vraies requêtes. AUTOKA_MAX_DETAILS (env)
34 34 > plafonne les vraies requêtes détail par synchronisation.
35 35
36 −## Complétude des champs (annonces actives, N = 455)
36 +## Complétude des champs (annonces actives, N = 456)
37 37
38 38 | Champ | % rempli |
39 39 |---|---|
@@ -46,14 +46,14 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
46 46 | Boîte (transmission) | 97.4 % |
47 47 | VIN | 100.0 % |
48 48 | Photos | 99.1 % |
49 −| Carfax | 69.2 % |
49 +| Carfax | 69.3 % |
50 50 | GPS (lat/lng) | 100.0 % |
51 51
52 52 ## Sources membres (BD live)
53 53
54 54 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
55 55 |---|---|---|---|---|---|---|---|
56 −| `megacentredeliquidation` | Méga Centre de liquidation | Mont-Joli | Bas-Saint-Laurent | 455 | 451 | 2026-08-17 23:35 | OK (455 trouvés) |
56 +| `megacentredeliquidation` | Méga Centre de liquidation | Mont-Joli | Bas-Saint-Laurent | 456 | 452 | 2026-08-18 05:33 | OK (453 trouvés) |
57 57
58 58 ## Erreurs de synchronisation récentes
59 59
modified docs/connecteurs/occasionbeaucage.md +1 −1
@@ -61,7 +61,7 @@ _Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas édite
61 61
62 62 | Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
63 63 |---|---|---|---|---|---|---|---|
64 −| `occasionbeaucage` | Occasion Beaucage | Sherbrooke | Estrie | 1226 | 192 | 2026-08-17 23:50 | OK (1226 trouvés) |
64 +| `occasionbeaucage` | Occasion Beaucage | Sherbrooke | Estrie | 1226 | 199 | 2026-08-17 23:50 | OK (1226 trouvés) |
65 65
66 66 ## Erreurs de synchronisation récentes
67 67
added docs/connecteurs/otogo.md +67 −0
@@ -0,0 +1,67 @@
1 +# Module `otogo` — Nuxt / API JSON publique /api-search/
2 +
3 +_Généré automatiquement par `scripts/gen_connector_docs.py` — ne pas éditer à la main._
4 +
5 +## Vue d'ensemble
6 +
7 +- **Fichier** : `autoka/connectors/otogo.py`
8 +- **Sources membres** : 1
9 +- **Annonces actives (BD)** : 23700
10 +- **Backend de fetch** : requests direct (aucun anti-bot)
11 +- **Mécanique détectée** : endpoint JSON
12 +- **Plafonds / cache** : AUTOKA_OTOGO_MAX
13 +- **Classes** : `OtogoQuebec` → `otogo`
14 +
15 +## Mécanique (en-tête du module)
16 +
17 +> connectors/otogo.py : PORTAIL — Otogo.ca (LC Media, 100 % Québec)
18 +>
19 +> Otogo expose une API JSON publique, sans clé ni rendu JS :
20 +> GET https://www.otogo.ca/api-search/?latlng=0,0&locale=fr&no_facets=
21 +> &rpp=100&page=N&status=Usagé,Certifié,Démo[&price=lo-hi]
22 +> Chaque annonce de la liste est déjà riche : titre/version FR, année,
23 +> prix, kilométrage, VIN (≈100 %), nom du concessionnaire et surtout le
24 +> GPS EXACT du vendeur (latitude/longitude) — parfait pour la recherche
25 +> par rayon et la dédup VIN avec les concessionnaires connectés.
26 +> (Paramètres découverts dans les bundles Nuxt : constantes c.b du module
27 +> 5731047.js — latlng/locale/rpp/page/price/status/no_facets/no_results.)
28 +>
29 +> Profondeur : l'API plafonne à ~1 000 résultats par requête (rpp=100 →
30 +> meta.pages ≤ 10) pour ~24 000 annonces d'occasion. On PARTITIONNE donc
31 +> par tranches de prix (bissection récursive + tranche ouverte « lo- ») :
32 +> chaque annonce n'est téléchargée qu'une fois, ~250 requêtes directes
33 +> par sync complet, zéro crédit d'API de scraping externe.
34 +>
35 +> Ville : absente de la liste — déduite du GPS exact via nearest_city()
36 +> (data/villes_gps.json) pour l'affichage et la région administrative.
37 +> Carburant/boîte : absents de la liste ; repli commun finalize() (jetons
38 +> du titre/de la version). Page détail : /auto/{slug_fr} (non visitée —
39 +> économie ; le VIN et le GPS sont déjà dans la liste).
40 +>
41 +> Plafond : AUTOKA_OTOGO_MAX (défaut 0 = inventaire complet).
42 +
43 +## Complétude des champs (annonces actives, N = 23 700)
44 +
45 +| Champ | % rempli |
46 +|---|---|
47 +| Marque | 99.9 % |
48 +| Modèle | 100.0 % |
49 +| Année | 100.0 % |
50 +| Kilométrage | 99.3 % |
51 +| Prix | 100.0 % |
52 +| Carburant | 7.7 % |
53 +| Boîte (transmission) | 0.0 % |
54 +| VIN | 99.3 % |
55 +| Photos | 95.9 % |
56 +| Carfax | 0.0 % |
57 +| GPS (lat/lng) | 100.0 % |
58 +
59 +## Sources membres (BD live)
60 +
61 +| Source | Nom | Ville | Région | Actives | Masquées (dup VIN) | Dernier sync | Statut |
62 +|---|---|---|---|---|---|---|---|
63 +| `otogo` | Otogo.ca | — | — | 23700 | 5847 | 2026-08-18 05:54 | OK (23700 trouvés) |
64 +
65 +## Erreurs de synchronisation récentes
66 +
67 +Aucune erreur dans le sync_log pour les sources de ce module.
modified docs/connecteurs/transverse-dedup-vin.md +10 −10
@@ -27,9 +27,9 @@ _Généré automatiquement par `scripts/gen_connector_docs.py`._
27 27
28 28 ## État live
29 29
30 −- **Annonces en base** : 23110
31 −- **Annonces masquées (`dup_of` non nul)** : 2315
32 −- **Annonces canoniques avec doublons** : 1729
30 +- **Annonces en base** : 49397
31 +- **Annonces masquées (`dup_of` non nul)** : 8438
32 +- **Annonces canoniques avec doublons** : 6655
33 33 - **Autorité** : concessionnaire direct > portail/regroupeur > petites annonces ; puis complétude de la fiche, puis ancienneté (`first_seen`).
34 34 - Les listes filtrent `dup_of IS NULL` ; les fiches détail restent accessibles par uid.
35 35
@@ -37,17 +37,17 @@ _Généré automatiquement par `scripts/gen_connector_docs.py`._
37 37
38 38 | Source | Annonces masquées |
39 39 |---|---|
40 −| `leprixdugros` | 653 |
41 −| `megacentredeliquidation` | 451 |
40 +| `otogo` | 5847 |
41 +| `leprixdugros` | 738 |
42 +| `megacentredeliquidation` | 452 |
42 43 | `montjolichrysler` | 382 |
43 −| `occasionbeaucage` | 192 |
44 +| `occasionbeaucage` | 199 |
45 +| `cargurus` | 170 |
44 46 | `touteslesmarques` | 169 |
45 −| `jeandumas` | 105 |
46 −| `jeandumashondabc` | 105 |
47 +| `jeandumas` | 108 |
47 48 | `abitibiautocredit` | 85 |
49 +| `jeandumasfordbc` | 76 |
48 50 | `baiecomeaugm` | 37 |
49 51 | `stefoychrysler` | 35 |
50 −| `dynastiehyundai` | 33 |
51 −| `gaspesiehyundai` | 30 |
52 52
53 53 Recalcul one-shot : `python3 -m autoka.dedup` (idempotent, relancé à la fin de chaque cycle d'ingestion).
modified scripts/gen_connector_docs.py +3 −1
@@ -6,7 +6,7 @@
6 6 #
7 7 # Génère docs/connecteurs/ (INDEX.md + une fiche par MODULE connecteur +
8 8 # fiches transverses) en croisant :
9 −# 1. data/sources.json — registre des sources (139 sources) ;
9 +# 1. data/sources.json — registre des sources (142 sources) ;
10 10 # 2. le code des connecteurs — en-tête « mécanique » de chaque module,
11 11 # backend de fetch, plafonds env (AUTOKA_*), classes exposées ;
12 12 # 3. la BD live data/autoka.db — volumétrie, complétude par champ,
@@ -58,6 +58,8 @@ HISTORIQUE = """\
58 58 |---|---|
59 59 | `2a35ac2` | Enrichissement connecteurs : Carfax, détails AED/LPDG, verticale moto, photos Magnetis, GPS concessionnaires |
60 60 | `bbc070b` | Vague 2 : dédup VIN inter-sources, Kijiji particuliers, rappels Transports Canada, coordonnées concessionnaires |
61 +| `b6d5459` | Documentation standardisée des connecteurs (générateur + fiches) |
62 +| Vague 3 | Grands portails : AutoTrader.ca/AutoHebdo (fenêtre récente), Otogo.ca (inventaire complet, VIN+GPS), CarGurus.ca (deal rating, via Scrapfly) |
61 63 """
62 64
63 65
64 66