import base64 import re from datetime import date, timedelta from urllib.parse import unquote from .base import BaseProfile # Hardcoded in the QBook JS bundle — fixed for all Hotels UK properties _AUTH_BASE = "5wv2AGgSxfRKvH92UzxHcbSRZNJBEMd8cAay2RY7qC8YBjSTdswMvvdw343X" _BUNDLE_K = "ad7ec113c738d3f5fe9f89a7e58a4b74e8301128" HEADERS = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", "Accept": "application/json", "Referer": "https://web-bookings.hotels.uk.com/", "Origin": "https://web-bookings.hotels.uk.com", } def _build_auth(hotel_id: str, from_date: str, to_date: str) -> str: code_auth = base64.b64encode( f"{hotel_id}#{from_date}#{to_date}".encode() ).decode().rstrip("=") return _AUTH_BASE[:30] + code_auth + _AUTH_BASE[30:] class QBookProfile(BaseProfile): name = "qbook" label = "QBook (Hotels UK)" required_params = [ {"key": "hotel_id", "label": "Hotel ID", "help": "Numeric hotel ID from the booking URL, e.g. 51420"}, {"key": "dk", "label": "Distribution Key", "help": "The k= value from the booking widget URL"}, ] @classmethod def detect(cls, url: str) -> dict | None: m = re.search(r'web-bookings\.hotels\.uk\.com/#/booking/(\d+)', url) if not m: return None hotel_id = m.group(1) dk_match = re.search(r'[?&]k=([^&\s#]+)', url) dk = unquote(dk_match.group(1)) if dk_match else "" return {"hotel_id": hotel_id, "dk": dk} async def fetch_category_names(self, client, params: dict) -> tuple[dict, dict]: hotel_id = params["hotel_id"] dk = params["dk"] from datetime import date, timedelta tomorrow = (date.today() + timedelta(days=1)).isoformat() day_after = (date.today() + timedelta(days=2)).isoformat() try: r = await client.get( "https://web-bookings.hotels.uk.com/api/pull", params={ "HotelAvailability": "", "k": _BUNDLE_K, "HotelID": hotel_id, "from": tomorrow, "to": day_after, "json": "", "dk": dk, "Qbook": "", "GHABooking": "", "CookieBooking": "", "COOKIE_HotelID": "", "guests": "", "adultsCheck": "", "childAges": "", "roomId": "", "revised": "", }, headers={**HEADERS, "Auth": _build_auth(hotel_id, tomorrow, day_after)}, timeout=20, ) except Exception: return {}, {} if r.status_code != 200: return {}, {} room_names: dict[str, str] = {} for item_id, item in r.json().get("availability", {}).items(): if isinstance(item, dict) and item.get("item_name"): room_names[str(item_id)] = item["item_name"] # QBook has no distinct rate plan names return room_names, {} async def fetch_arrival_dates(self, client, params: dict) -> list[str]: today = date.today() return [(today + timedelta(days=i)).isoformat() for i in range(1, 91)] async def fetch_night_rates(self, client, params: dict, arrival: str, nights: int = 1) -> list[dict]: hotel_id = params["hotel_id"] dk = params["dk"] departure = (date.fromisoformat(arrival) + timedelta(days=nights)).isoformat() try: r = await client.get( "https://web-bookings.hotels.uk.com/api/pull", params={ "HotelAvailability": "", "k": _BUNDLE_K, "HotelID": hotel_id, "from": arrival, "to": departure, "json": "", "dk": dk, "Qbook": "", "GHABooking": "", "CookieBooking": "", "COOKIE_HotelID": "", "guests": "", "adultsCheck": "", "childAges": "", "roomId": "", "revised": "", }, headers={**HEADERS, "Auth": _build_auth(hotel_id, arrival, departure)}, timeout=20, ) except Exception: return [] if r.status_code != 200: return [] availability = r.json().get("availability", {}) if not isinstance(availability, dict): return [] rooms = [] for item_id, item in availability.items(): if not isinstance(item, dict): continue avail_count = item.get("availableroom", 0) price = item.get("roomprice", 0.0) # roomprice=0 on an available room means no rate configured in QBook's backend if not price: continue rooms.append({ "roomId": str(item_id), "rateId": "default", "availability": avail_count, "prices": [{"amountBeforeTax": price, "amountAfterTax": price}] * nights, "currencyCode": "GBP", }) return rooms