# ----------------------------------------------------------------------------- # Immo-Ka — Agrégateur de maisons à vendre (province de Québec) # Auteur : Simon-Pierre Boucher — contact@spboucher.ai # connectors/barnes_quebec.py : BARNES Québec (barnes-quebec.com) # Site WordPress indexé dans Algolia. La config expose l'App ID et une clé # API dans le HTML ; l'index « quebec_all » contient les propriétés (type # "property") avec adresse, prix, MLS, chambres, salles de bains, superficie # et géolocalisation. Les enregistrements sont dupliqués par langue → on # dédoublonne par référence MLS. Les permaliens pointent vers le domaine de # staging Kinsta ; on les réécrit vers le domaine de production. # ----------------------------------------------------------------------------- from __future__ import annotations import os import re from .base import BaseConnector from . import _detailutil as du from ..schema import PropertyListing DETAIL_LIMIT = int(os.environ.get("IMMOKA_BARNES_DETAIL_LIMIT", "400")) # Galerie WordPress : .../wp-content/uploads/AAAA/MM/{ref}-{hash}-{L}x{H}.jpg _IMG_RE = re.compile( r'https://barnes-quebec\.com/wp-content/uploads/\d{4}/\d{2}/[^"\'\\ ]+?\.(?:jpg|jpeg|png|webp)', re.I) _SIZE_RE = re.compile(r'-(\d{2,4})x(\d{2,4})(?=\.[a-z]+$)', re.I) APP_ID = "HCW55VIQNM" API_KEY = "f4a20779fb6ded84a9c96a9b5976328b" INDEX = "quebec_all" QUERY_URL = f"https://{APP_ID}-dsn.algolia.net/1/indexes/{INDEX}/query" SITE = "https://barnes-quebec.com" STAGING = "stg-quebec-staging.kinsta.cloud" HITS_PER_PAGE = 100 MAX_PAGES = 40 class BarnesQuebecConnector(BaseConnector): source_id = "barnes_quebec" request_delay = 0.25 use_detail_cache = False def fetch(self) -> list[PropertyListing]: best: dict[str, tuple[int, PropertyListing]] = {} page = 0 while page < MAX_PAGES: data = self._query(page) hits = data.get("hits", []) if not hits: break for h in hits: if h.get("type") != "property": continue lst = self._to_listing(h) if lst is None: continue # une propriété apparaît en plusieurs langues et parfois en # doublon « Contact us » (sans prix). Clé stable = adresse # normalisée ; on garde la meilleure fiche (prix connu + FR). key = " ".join(lst.title.split()).lower() score = (2 if lst.price else 0) + (1 if h.get("lang_fr") == 1 else 0) if key not in best or score > best[key][0]: best[key] = (score, lst) if page + 1 >= data.get("nbPages", 0): break page += 1 listings = [lst for _, lst in best.values()] # Algolia ne contient pas les photos : on les récupère sur la fiche. du.enrich(self, listings, DETAIL_LIMIT, parse_barnes_detail, key="v2") return listings def _query(self, page: int) -> dict: resp = self.post( QUERY_URL, headers={"X-Algolia-API-Key": API_KEY, "X-Algolia-Application-Id": APP_ID, "Content-Type": "application/json"}, json={"params": f"hitsPerPage={HITS_PER_PAGE}&page={page}"}, ) return resp.json() def _to_listing(self, h: dict) -> PropertyListing | None: object_id = str(h.get("objectID") or "") mls = str(h.get("property_mls_reference") or "").strip() if not object_id and not mls: return None permalink = (h.get("permalink") or "").replace(STAGING, "barnes-quebec.com") if permalink.startswith("http://"): permalink = "https://" + permalink[len("http://"):] price = h.get("property_price") or 0 try: price = float(price) except (TypeError, ValueError): price = 0.0 price_label = h.get("property_pretty_price") or "" area = _pos(h.get("property_area")) land = _pos(h.get("property_land_area")) lat = h.get("property_address_latitude") or None lng = h.get("property_address_longitude") or None try: lat = float(lat) if lat else None lng = float(lng) if lng else None except (TypeError, ValueError): lat = lng = None if lat == 0 or lng == 0: lat = lng = None return PropertyListing( source=self.source_id, external_id=mls or object_id, url=permalink or SITE + "/property/", title=(h.get("title") or "").strip(), address=(h.get("title") or "").strip(), city=(h.get("property_address_city") or "").strip(), region=(h.get("region") or "").strip(), property_type=(h.get("property_type") or "").strip(), price=price if price > 0 else None, price_label=price_label, bedrooms=_pos(h.get("property_bedrooms_integer")), bathrooms=_pos(h.get("property_bathrooms")), area_sqft=area, lot_sqft=land, mls=mls, description=(h.get("content") or "")[:4000], lat=lat, lng=lng, broker_name="BARNES Québec", ) def _pos(v): try: n = float(v) return int(n) if n and n > 0 else None except (TypeError, ValueError): return None def parse_barnes_detail(html: str) -> dict: """Galerie photo pleine résolution (absente d'Algolia) + description/pièces.""" out: dict = {} # regroupe par image de base (sans le suffixe -LxH), garde la plus grande best: dict[str, tuple[int, str]] = {} for u in _IMG_RE.findall(html): m = _SIZE_RE.search(u) area = int(m.group(1)) * int(m.group(2)) if m else 10 ** 8 # sans suffixe = original base = _SIZE_RE.sub("", u) if base not in best or area > best[base][0]: best[base] = (area, u) imgs = [u for _, u in best.values()] # ignore les vignettes de courtiers/logos (gardent souvent une réf MLS chiffrée) imgs = [u for u in imgs if re.search(r"/\d{6,}", u)] or imgs if imgs: out["images"] = imgs[:60] desc = du.ld_description(html) if desc: out["description"] = desc _det = du.centris_details(du.flatten(html)) if _det: out.setdefault("details", {}).update(_det) return out