# ----------------------------------------------------------------------------- # Lou-Ka — Location court terme # connectors/homminichalets.py : HOM Mini Chalets (homminichalets.com) — # 12 mini-chalets numérotés (01. Le Renard … 12. Le Loup) à Val-des-Monts # (Outaouais), en deux gammes : « avec spa » (01-08) et « avec circuit # thermal » (09-12, spa + sauna + hammam + douche froide). # # Méthode : le site Shopify renvoie un 429 systématique en direct → Scrapfly. # 1. UIDS : /pages/chalets (rendu JS, le calendrier Hostfully est monté en # JS) → 12 liens /pages/chalets?uid= ; la page liste aussi les noms # groupés par gamme (« Mini chalets avec circuit thermal » précède # 09-12) et le pied de page porte les 2 adresses + le CITQ commun 298559. # 2. DÉTAIL : l'API publique JSONP du moteur Hostfully répond en DIRECT # (pas de 429) : platform.hostfully.com/getproperty_api.jsp?propertyUID=… # &aid=ORB-… → {"price": 300, "maximumGuests": 2, "minStay": 1, # "name": "07. Le Huard"}. Pas d'endpoint photos public → galerie # GÉNÉRIQUE du site (01.jpg-25.jpg de l'accueil, via Scrapfly sans # render_js), signalée par details.images_generic. Commodités/description # par gamme (bandeaux de l'accueil), adresses par numéro d'unité # (07-08 = chemin du Saphir, le reste = chemin du Rubis). # ----------------------------------------------------------------------------- from __future__ import annotations import json import re import sys from ..schema import StListing from .base import StConnector SITE = "https://homminichalets.com" LIST_URL = SITE + "/pages/chalets" # API publique du widget de réservation Hostfully (accessible en direct) AID = "ORB-49587220416635719" API = ("https://platform.hostfully.com/getproperty_api.jsp" "?jsoncallback=cb&propertyUID={uid}&aid=" + AID) CITQ = "298559" # gammes (bandeaux de l'accueil) : commodités + description _SPA_AMEN = ["Spa privé", "Chaise hamac", "Lit King", "Foyer intérieur", "Plancher chauffant"] _SPA_DESC = ("Mini chalet avec spa. Profitez d'un mini chalet de luxe tout " "équipé avec spa privé sur la galerie, foyer intérieur et " "plancher chauffant.") _THERMAL_AMEN = ["Spa", "Sauna", "Hammam", "Douche froide", "Lit Queen", "Foyer intérieur", "Plancher chauffant"] _THERMAL_DESC = ("Mini chalet avec circuit thermal (spa, sauna, hammam, " "douche froide). Vivez une expérience de détente privée et " "luxueuse dans un mini chalet tout équipé avec foyer " "intérieur et plancher chauffant.") class HomMiniChalets(StConnector): source_id = "homminichalets" request_delay = 1.0 # -- découverte des uids (Scrapfly, calendrier monté en JS) --------------- def _uids(self) -> list[str]: h = self.get_scrapfly(LIST_URL, render_js=True, rendering_wait=4000) uids: list[str] = [] for u in re.findall(r'href="[^"]*?/pages/chalets\?uid=' r'([0-9a-f-]{36})"', h): if u not in uids: uids.append(u) return uids # -- galerie générique du site (accueil Shopify statique) ----------------- def _site_images(self) -> list[str]: try: h = self.get_scrapfly(SITE + "/", render_js=False) except Exception as exc: # noqa: BLE001 print(f"[homminichalets] accueil : {exc}", file=sys.stderr) return [] imgs: list[str] = [] for num, v in re.findall(r"//homminichalets\.com/cdn/shop/files/" r"(\d{2}\.jpg)\?v=(\d+)", h): u = f"{SITE}/cdn/shop/files/{num}?v={v}&width=1600" if u not in imgs: imgs.append(u) return imgs[:15] # -- fiche Hostfully (JSONP, en direct) ------------------------------------ def _detail(self, uid: str) -> dict: txt = self.get(API.format(uid=uid)).text.strip() m = re.match(r"(?s)cb\(true,(\{.*\})\)$", txt) return json.loads(m.group(1)) if m else {} # -- contrat --------------------------------------------------------------- def fetch(self) -> list[StListing]: uids = self._uids() images = self._site_images() if uids else [] listings: list[StListing] = [] for uid in uids: det = self.detail(uid, "v1", lambda u=uid: self._detail(u)) name = str(det.get("name") or "").strip() if not name: continue m = re.match(r"(\d{1,2})\.", name) num = int(m.group(1)) if m else 0 thermal = num >= 9 address = ("32 chemin du Saphir" if num in (7, 8) else "154 chemin du Rubis") price = det.get("price") price = float(price) if price and 20 <= float(price) <= 20000 \ else None details = { "gamme": ("circuit thermal" if thermal else "spa"), "domain": "HOM Mini Chalets", } if images: details["images_generic"] = True if det.get("minStay"): details["min_stay"] = f"{det['minStay']} nuit(s)" listings.append(StListing( source=self.source_id, external_id=uid, url=f"{LIST_URL}?uid={uid}", title=f"{name} — HOM Mini Chalets", property_type="Mini-chalet", address=address, city="Val-des-Monts", region="Outaouais", price_night=price, price_label=(f"à partir de {price:g} $ / nuit" if price else ""), capacity=(float(det["maximumGuests"]) if det.get("maximumGuests") else None), bedrooms=1.0, citq=CITQ, description=_THERMAL_DESC if thermal else _SPA_DESC, amenities=list(_THERMAL_AMEN if thermal else _SPA_AMEN), details=details, images=list(images), )) return listings