SPB Git forge

spb/lou-ka

Public

Lou·Ka — tous les logements à louer du Québec, un seul endroit.

232commits 1branches 0releases
172.9 MBsize
maindefault branch
2 days agolast push
HTML 98.9% Python 0.6%
6.1 KB · 143 lines python
Raw Blame History
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