# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/accestremblant.py : Accès Tremblant (accestremblant.ca) # # Agence de condos à Mont-Tremblant (~17 fiches) — WordPress Avada # (portfolio) + moteur Guesty (guestybookings.com). # # Méthode : # 1. LISTE : sitemap https://accestremblant.ca/avada_portfolio-sitemap.xml # → /condos// (lastmod = clé du cache détail). # 2. DÉTAIL (cache self.detail) : la fiche WP fournit spécs (spans # icon-condos : « 8 Personnes », « 3 chambres », « 3 salles de bain » + # extras type « Foyer au gaz »), description (JSON-LD Yoast), photos # (wp-content/uploads) et l'id Guesty (lien guestybookings.com). # 3. PRIX : chaque fiche contient un carrousel « autres condos » avec # « À partir de N $ » pour les AUTRES fiches ; chaque payload détail # mémorise cette carte slug→prix et fetch() fusionne le tout (le prix # d'une fiche vient donc des autres pages, même à froid depuis le cache). # External_id = slug WP (stable) ; l'id Guesty est gardé dans details. # ----------------------------------------------------------------------------- from __future__ import annotations import html as _html import json import re from ..schema import StListing from .base import StConnector SITE = "https://accestremblant.ca" SITEMAP = f"{SITE}/avada_portfolio-sitemap.xml" _TAG_RE = re.compile(r"<[^>]+>") def _text(fragment: str) -> str: return _html.unescape(re.sub(r"\s+", " ", _TAG_RE.sub(" ", fragment))).strip() class AccesTremblant(StConnector): source_id = "accestremblant" # -- liste (sitemap) ---------------------------------------------------- def _sitemap(self) -> list[tuple[str, str]]: """[(url fiche, lastmod)] — dédupliqué, sans la page index.""" xml = self.get(SITEMAP).text seen: dict[str, str] = {} for m in re.finditer(r"(?s)\s*([^<]+)" r"(?:\s*([^<]+))?", xml): url, lastmod = m.group(1).strip(), (m.group(2) or "").strip() if re.fullmatch(rf"{re.escape(SITE)}/condos/[^/]+/", url): seen.setdefault(url, lastmod) return sorted(seen.items()) # -- page détail ------------------------------------------------------ def _detail(self, url: str) -> dict: h = self.get(url).text d: dict = {} # spécs : … 8 Personnes / 3 chambres / … extras: list[str] = [] for raw in re.findall(r'(?s)]*>(.*?)', h): t = _text(raw) if not t: continue m = re.match(r"(\d+)\s*[Pp]ersonnes?", t) if m: d["capacity"] = float(m.group(1)) continue m = re.match(r"(\d+)\s*[Cc]hambres?", t) if m: d["bedrooms"] = float(m.group(1)) continue m = re.match(r"(\d+)\s*[Ss]alles?\s*de\s*bain", t) if m: d["bathrooms"] = float(m.group(1)) continue if len(t) <= 60: extras.append(t) if extras: d["amenities"] = extras # description : JSON-LD Yoast (WebPage.description) m = re.search(r'(?s)", h) if m: try: graph = json.loads(m.group(1)).get("@graph") or [] for node in graph: if node.get("@type") == "WebPage" and node.get("description"): d["description"] = _text(node["description"])[:4000] break except ValueError: pass # id Guesty (lien « réserver » guestybookings.com) m = re.search(r"guestybookings\.com/(?:fr/)?properties/([a-f0-9]{24})", h) if m: d["guesty_id"] = m.group(1) # photos : uploads WP (originaux, sans logos ni vignettes -NxN) imgs: list[str] = [] for u in re.findall(r'(https://accestremblant\.ca/wp-content/uploads/' r'20\d\d/\d\d/[^" ]+\.(?:jpe?g|png|webp))', h): if re.search(r"-\d{2,4}x\d{2,4}\.", u): continue if re.search(r"logo|favicon|icon", u, re.I): continue if u not in imgs: imgs.append(u) d["images"] = imgs[:20] # carte des prix « autres condos » : slug → à partir de N $ prices: dict[str, float] = {} for slug, val in re.findall( r'href="https://accestremblant\.ca/condos/([^"/]+)/"[^>]*>' r"\s*À partir de\s*([\d ,]+)\s*\$", h): try: prices[slug] = float(val.replace(" ", "").replace(",", ".")) except ValueError: continue d["prices_seen"] = prices return d # -- contrat ---------------------------------------------------------- def fetch(self) -> list[StListing]: rows = [] price_map: dict[str, float] = {} for url, lastmod in self._sitemap(): slug = url.rstrip("/").rsplit("/", 1)[-1] try: det = self.detail(slug, lastmod, lambda u=url: self._detail(u)) except Exception: continue price_map.update(det.get("prices_seen") or {}) rows.append((slug, url, det)) listings: list[StListing] = [] for slug, url, det in rows: title = slug.replace("-", " ").title() desc = det.get("description") or "" m = re.match(r"([^|]{2,60})\|", desc) if m: # « Verbier C | Découvrez… » title = m.group(1).strip() desc = desc.split("|", 1)[1].strip() price = price_map.get(slug) details = {k: v for k, v in { "guesty_id": det.get("guesty_id"), }.items() if v} listings.append(StListing( source=self.source_id, external_id=slug, url=url, title=title, property_type="Condo", city="Mont-Tremblant", region="Laurentides", price_night=price, price_label=(f"à partir de {price:.0f} $ / nuit" if price else ""), capacity=det.get("capacity"), bedrooms=det.get("bedrooms"), bathrooms=det.get("bathrooms"), description=desc[:4000], amenities=det.get("amenities") or [], details=details, images=det.get("images") or [], )) return listings