Lou·Ka — tous les logements à louer du Québec, un seul endroit.
HTML 98.9%
Python 0.6%
1# -----------------------------------------------------------------------------2# Lou-Ka — Location court terme3# connectors/homminichalets.py : HOM Mini Chalets (homminichalets.com) —4# 12 mini-chalets numérotés (01. Le Renard … 12. Le Loup) à Val-des-Monts5# (Outaouais), en deux gammes : « avec spa » (01-08) et « avec circuit6# thermal » (09-12, spa + sauna + hammam + douche froide).7#8# Méthode : le site Shopify renvoie un 429 systématique en direct → Scrapfly.9# 1. UIDS : /pages/chalets (rendu JS, le calendrier Hostfully est monté en10# JS) → 12 liens /pages/chalets?uid=<uuid> ; la page liste aussi les noms11# groupés par gamme (« Mini chalets avec circuit thermal » précède12# 09-12) et le pied de page porte les 2 adresses + le CITQ commun 298559.13# 2. DÉTAIL : l'API publique JSONP du moteur Hostfully répond en DIRECT14# (pas de 429) : platform.hostfully.com/getproperty_api.jsp?propertyUID=…15# &aid=ORB-… → {"price": 300, "maximumGuests": 2, "minStay": 1,16# "name": "07. Le Huard"}. Pas d'endpoint photos public → galerie17# GÉNÉRIQUE du site (01.jpg-25.jpg de l'accueil, via Scrapfly sans18# render_js), signalée par details.images_generic. Commodités/description19# par gamme (bandeaux de l'accueil), adresses par numéro d'unité20# (07-08 = chemin du Saphir, le reste = chemin du Rubis).21# -----------------------------------------------------------------------------22from __future__ import annotations2324import json25import re26import sys2728from ..schema import StListing29from .base import StConnector3031SITE = "https://homminichalets.com"32LIST_URL = SITE + "/pages/chalets"3334# API publique du widget de réservation Hostfully (accessible en direct)35AID = "ORB-49587220416635719"36API = ("https://platform.hostfully.com/getproperty_api.jsp"37 "?jsoncallback=cb&propertyUID={uid}&aid=" + AID)3839CITQ = "298559"4041# gammes (bandeaux de l'accueil) : commodités + description42_SPA_AMEN = ["Spa privé", "Chaise hamac", "Lit King",43 "Foyer intérieur", "Plancher chauffant"]44_SPA_DESC = ("Mini chalet avec spa. Profitez d'un mini chalet de luxe tout "45 "équipé avec spa privé sur la galerie, foyer intérieur et "46 "plancher chauffant.")47_THERMAL_AMEN = ["Spa", "Sauna", "Hammam", "Douche froide", "Lit Queen",48 "Foyer intérieur", "Plancher chauffant"]49_THERMAL_DESC = ("Mini chalet avec circuit thermal (spa, sauna, hammam, "50 "douche froide). Vivez une expérience de détente privée et "51 "luxueuse dans un mini chalet tout équipé avec foyer "52 "intérieur et plancher chauffant.")535455class HomMiniChalets(StConnector):56 source_id = "homminichalets"57 request_delay = 1.05859 # -- découverte des uids (Scrapfly, calendrier monté en JS) ---------------60 def _uids(self) -> list[str]:61 h = self.get_scrapfly(LIST_URL, render_js=True, rendering_wait=4000)62 uids: list[str] = []63 for u in re.findall(r'href="[^"]*?/pages/chalets\?uid='64 r'([0-9a-f-]{36})"', h):65 if u not in uids:66 uids.append(u)67 return uids6869 # -- galerie générique du site (accueil Shopify statique) -----------------70 def _site_images(self) -> list[str]:71 try:72 h = self.get_scrapfly(SITE + "/", render_js=False)73 except Exception as exc: # noqa: BLE00174 print(f"[homminichalets] accueil : {exc}", file=sys.stderr)75 return []76 imgs: list[str] = []77 for num, v in re.findall(r"//homminichalets\.com/cdn/shop/files/"78 r"(\d{2}\.jpg)\?v=(\d+)", h):79 u = f"{SITE}/cdn/shop/files/{num}?v={v}&width=1600"80 if u not in imgs:81 imgs.append(u)82 return imgs[:15]8384 # -- fiche Hostfully (JSONP, en direct) ------------------------------------85 def _detail(self, uid: str) -> dict:86 txt = self.get(API.format(uid=uid)).text.strip()87 m = re.match(r"(?s)cb\(true,(\{.*\})\)$", txt)88 return json.loads(m.group(1)) if m else {}8990 # -- contrat ---------------------------------------------------------------91 def fetch(self) -> list[StListing]:92 uids = self._uids()93 images = self._site_images() if uids else []9495 listings: list[StListing] = []96 for uid in uids:97 det = self.detail(uid, "v1", lambda u=uid: self._detail(u))98 name = str(det.get("name") or "").strip()99 if not name:100 continue101102 m = re.match(r"(\d{1,2})\.", name)103 num = int(m.group(1)) if m else 0104 thermal = num >= 9105 address = ("32 chemin du Saphir" if num in (7, 8)106 else "154 chemin du Rubis")107108 price = det.get("price")109 price = float(price) if price and 20 <= float(price) <= 20000 \110 else None111112 details = {113 "gamme": ("circuit thermal" if thermal else "spa"),114 "domain": "HOM Mini Chalets",115 }116 if images:117 details["images_generic"] = True118 if det.get("minStay"):119 details["min_stay"] = f"{det['minStay']} nuit(s)"120121 listings.append(StListing(122 source=self.source_id,123 external_id=uid,124 url=f"{LIST_URL}?uid={uid}",125 title=f"{name} — HOM Mini Chalets",126 property_type="Mini-chalet",127 address=address,128 city="Val-des-Monts",129 region="Outaouais",130 price_night=price,131 price_label=(f"à partir de {price:g} $ / nuit"132 if price else ""),133 capacity=(float(det["maximumGuests"])134 if det.get("maximumGuests") else None),135 bedrooms=1.0,136 citq=CITQ,137 description=_THERMAL_DESC if thermal else _SPA_DESC,138 amenities=list(_THERMAL_AMEN if thermal else _SPA_AMEN),139 details=details,140 images=list(images),141 ))142 return listings143