Skip to content

Commit 6bd00fd

Browse files
authored
Create Accommodation-Recommendation-Engine-Booking.py
1 parent 33df1c0 commit 6bd00fd

1 file changed

Lines changed: 309 additions & 0 deletions

File tree

Lines changed: 309 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,309 @@
1+
import threading
2+
import requests
3+
import webbrowser
4+
import tkinter as tk
5+
from tkinter import messagebox
6+
from dataclasses import dataclass
7+
from typing import List, Dict
8+
import io
9+
import json
10+
import ttkbootstrap as tb
11+
from ttkbootstrap.widgets.scrolled import ScrolledText
12+
from PIL import Image, ImageTk
13+
14+
# ---------------- CONFIG ---------------- #
15+
GOOGLE_API_KEY = "YOUR_GOOGLE_API_KEY"
16+
AFFILIATE_ID = "YOUR_AFFILIATE_ID"
17+
API_TOKEN = "YOUR_API_TOKEN"
18+
BASE_URL = "https://demandapi.booking.com/3.1"
19+
RESULTS_PER_PAGE = 6
20+
FAVORITES_FILE = "accommodation_favorites.json"
21+
22+
HEADERS = {
23+
"Content-Type": "application/json",
24+
"Authorization": f"Bearer {API_TOKEN}",
25+
"X-Affiliate-Id": str(AFFILIATE_ID)
26+
}
27+
28+
# ---------------- IMAGE CACHE ---------------- #
29+
cover_cache: Dict[str, ImageTk.PhotoImage] = {}
30+
31+
def load_cover(url: str, size=(120, 80)):
32+
if not url:
33+
return None
34+
if url in cover_cache:
35+
return cover_cache[url]
36+
try:
37+
img = Image.open(io.BytesIO(requests.get(url).content)).resize(size)
38+
photo = ImageTk.PhotoImage(img)
39+
cover_cache[url] = photo
40+
return photo
41+
except:
42+
return None
43+
44+
# ---------------- DATA STRUCTURE ---------------- #
45+
@dataclass
46+
class Accommodation:
47+
name: str
48+
city: str
49+
price: float
50+
rating: float
51+
url: str
52+
image_url: str = "" # for preview images
53+
54+
# ---------------- ENGINE ---------------- #
55+
class AccommodationEngine:
56+
def __init__(self):
57+
self.results: List[Accommodation] = []
58+
self.current_page = 1
59+
self.favorites: List[Accommodation] = []
60+
self.selected_latlng = (None, None)
61+
self.load_favorites()
62+
self.build_ui()
63+
64+
# ---------------- FAVORITES ---------------- #
65+
def load_favorites(self):
66+
try:
67+
with open(FAVORITES_FILE, "r", encoding="utf-8") as f:
68+
self.favorites = [Accommodation(**a) for a in json.load(f)]
69+
except FileNotFoundError:
70+
self.favorites = []
71+
72+
def save_favorites(self):
73+
with open(FAVORITES_FILE, "w", encoding="utf-8") as f:
74+
json.dump([a.__dict__ for a in self.favorites], f, indent=2)
75+
76+
def add_to_favorites(self, acc: Accommodation):
77+
if acc not in self.favorites:
78+
self.favorites.append(acc)
79+
self.save_favorites()
80+
messagebox.showinfo("Saved", f"Added {acc.name} to Favorites")
81+
82+
# ---------------- GOOGLE PLACES AUTOCOMPLETE ---------------- #
83+
def autocomplete_city(self, query: str):
84+
url = f"https://maps.googleapis.com/maps/api/place/autocomplete/json"
85+
params = {"input": query, "types": "(cities)", "key": GOOGLE_API_KEY}
86+
r = requests.get(url, params=params)
87+
return r.json().get("predictions", [])
88+
89+
def get_city_location(self, place_id: str):
90+
url = f"https://maps.googleapis.com/maps/api/place/details/json"
91+
params = {"place_id": place_id, "fields": "geometry", "key": GOOGLE_API_KEY}
92+
r = requests.get(url, params=params)
93+
geom = r.json().get("result", {}).get("geometry", {})
94+
loc = geom.get("location", {})
95+
return loc.get("lat"), loc.get("lng")
96+
97+
# ---------------- BOOKING.COM SEARCH ---------------- #
98+
def fetch_accommodations(self, lat, lng, radius, min_price, max_price, min_rating, amenities, room_types):
99+
body = {
100+
"geo": {"lat": lat, "lng": lng, "radius": radius},
101+
"currency": "USD",
102+
"guests": {"number_of_adults": 2, "number_of_rooms": 1},
103+
"price": {"minimum": min_price, "maximum": max_price},
104+
"rating": {"minimum_review_score": min_rating},
105+
"accommodation_types": room_types,
106+
"room_facilities": amenities,
107+
"rows": 100,
108+
"extras": ["extra_charges", "products"]
109+
}
110+
r = requests.post(f"{BASE_URL}/accommodations/search", headers=HEADERS, json=body)
111+
data = r.json().get("data", [])
112+
results = []
113+
for item in data:
114+
try:
115+
name = item.get("name")
116+
city = item.get("address", {}).get("city_name", "")
117+
price = float(item.get("price", {}).get("total", 0))
118+
rating = float(item.get("review_score", 0))
119+
url = item.get("url")
120+
image_url = item.get("photo_main", "")
121+
results.append(Accommodation(name, city, price, rating, url, image_url))
122+
except:
123+
continue
124+
return results
125+
126+
# ---------------- UI ---------------- #
127+
def build_ui(self):
128+
self.app = tb.Window(title="Accommodation Engine", themename="darkly", size=(1000, 700))
129+
130+
top = tb.Frame(self.app, padding=15)
131+
top.pack(fill=tk.X)
132+
133+
tb.Label(top, text="🏨 Accommodation Finder", font=("Segoe UI", 18, "bold")).pack(anchor=tk.W)
134+
135+
# City search with autocomplete
136+
self.city_var = tk.StringVar()
137+
city_entry = tb.Entry(top, textvariable=self.city_var, font=("Segoe UI", 13))
138+
city_entry.pack(fill=tk.X, pady=4)
139+
city_entry.bind("<KeyRelease>", self.on_city_key)
140+
141+
self.city_suggestions = tk.Listbox(top, height=5)
142+
self.city_suggestions.pack(fill=tk.X)
143+
self.city_suggestions.bind("<<ListboxSelect>>", self.on_city_select)
144+
145+
# Distance radius slider
146+
tb.Label(top, text="Distance Radius (km)").pack(anchor=tk.W)
147+
self.radius_var = tk.DoubleVar(value=5)
148+
tb.Scale(top, from_=1, to=50, variable=self.radius_var, orient="horizontal").pack(fill=tk.X, pady=2)
149+
150+
# Filters
151+
filter_frame = tb.Frame(top)
152+
filter_frame.pack(fill=tk.X)
153+
self.min_price = tk.StringVar(value="0")
154+
self.max_price = tk.StringVar(value="1000")
155+
self.min_rating = tk.StringVar(value="0")
156+
tb.Entry(filter_frame, textvariable=self.min_price, width=8).pack(side=tk.LEFT, padx=2)
157+
tb.Entry(filter_frame, textvariable=self.max_price, width=8).pack(side=tk.LEFT, padx=2)
158+
tb.Entry(filter_frame, textvariable=self.min_rating, width=8).pack(side=tk.LEFT, padx=2)
159+
tb.Label(filter_frame, text="MinPrice MaxPrice MinRating").pack(side=tk.LEFT, padx=5)
160+
161+
# Amenity checkboxes
162+
amenity_frame = tb.LabelFrame(top, text="Amenities")
163+
amenity_frame.pack(fill=tk.X, pady=2)
164+
self.amenity_vars = {}
165+
amenities = ["Pool", "Wifi", "Parking", "Gym"]
166+
for a in amenities:
167+
var = tk.IntVar()
168+
self.amenity_vars[a] = var
169+
tb.Checkbutton(amenity_frame, text=a, variable=var).pack(side=tk.LEFT, padx=5)
170+
171+
# Room type checkboxes
172+
room_frame = tb.LabelFrame(top, text="Room Types")
173+
room_frame.pack(fill=tk.X, pady=2)
174+
self.room_vars = {}
175+
room_types = ["Hotel", "Apartment", "Villa", "B&B"]
176+
for r in room_types:
177+
var = tk.IntVar()
178+
self.room_vars[r] = var
179+
tb.Checkbutton(room_frame, text=r, variable=var).pack(side=tk.LEFT, padx=5)
180+
181+
tb.Button(top, text="Search", bootstyle="primary", command=self.perform_search).pack(pady=6)
182+
183+
# Results
184+
result_frame = tb.Frame(self.app)
185+
result_frame.pack(fill=tk.BOTH, expand=True)
186+
self.result_box = ScrolledText(result_frame)
187+
self.result_box.pack(fill=tk.BOTH, expand=True)
188+
self.text = self.result_box.text
189+
self.text.configure(state="disabled", wrap="word")
190+
191+
# Pagination & favorites
192+
nav = tb.Frame(self.app)
193+
nav.pack(fill=tk.X)
194+
tb.Button(nav, text="← Prev", command=self.prev_page).pack(side=tk.LEFT)
195+
self.page_label = tb.Label(nav, text="Page 1"); self.page_label.pack(side=tk.LEFT, padx=10)
196+
tb.Button(nav, text="Next →", command=self.next_page).pack(side=tk.LEFT)
197+
tb.Button(nav, text="Favorites", bootstyle="success", command=self.show_favorites).pack(side=tk.RIGHT)
198+
199+
self.app.mainloop()
200+
201+
# ---------------- CITY AUTOCOMPLETE EVENTS ---------------- #
202+
def on_city_key(self, event):
203+
query = self.city_var.get()
204+
if len(query) < 2:
205+
return
206+
suggestions = self.autocomplete_city(query)
207+
self.city_suggestions.delete(0, tk.END)
208+
for s in suggestions:
209+
self.city_suggestions.insert(tk.END, f"{s['description']}||{s['place_id']}")
210+
211+
def on_city_select(self, event):
212+
sel = self.city_suggestions.curselection()
213+
if sel:
214+
val = self.city_suggestions.get(sel[0])
215+
desc, pid = val.split("||")
216+
self.city_var.set(desc)
217+
lat, lng = self.get_city_location(pid)
218+
self.selected_latlng = (lat, lng)
219+
self.city_suggestions.delete(0, tk.END)
220+
221+
# ---------------- SEARCH ---------------- #
222+
def perform_search(self):
223+
if not self.selected_latlng[0]:
224+
messagebox.showerror("Error", "Select a city from suggestions")
225+
return
226+
lat, lng = self.selected_latlng
227+
radius = self.radius_var.get()
228+
min_p = float(self.min_price.get())
229+
max_p = float(self.max_price.get())
230+
min_r = float(self.min_rating.get())
231+
amenities = [i for i, v in self.amenity_vars.items() if v.get() == 1]
232+
room_types = [i for i, v in self.room_vars.items() if v.get() == 1]
233+
234+
threading.Thread(target=self.search_thread,
235+
args=(lat, lng, radius, min_p, max_p, min_r, amenities, room_types), daemon=True).start()
236+
237+
def search_thread(self, lat, lng, radius, min_p, max_p, min_r, amenities, room_types):
238+
self.results = self.fetch_accommodations(lat, lng, radius, min_p, max_p, min_r, amenities, room_types)
239+
self.current_page = 1
240+
self.display_page()
241+
242+
# ---------------- DISPLAY WITH IMAGE ---------------- #
243+
def display_page(self):
244+
self.text.configure(state="normal")
245+
self.text.delete("1.0", "end")
246+
start = (self.current_page - 1) * RESULTS_PER_PAGE
247+
end = start + RESULTS_PER_PAGE
248+
for acc in self.results[start:end]:
249+
self.insert_accommodation(self.text, acc)
250+
self.text.configure(state="disabled")
251+
self.page_label.config(text=f"Page {self.current_page}")
252+
253+
def insert_accommodation(self, container_widget, acc: Accommodation):
254+
frame = tk.Frame(container_widget)
255+
art_label = tk.Label(frame)
256+
art_label.pack(side=tk.LEFT, padx=5, pady=5)
257+
258+
info_frame = tk.Frame(frame)
259+
info_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
260+
261+
tb.Label(info_frame, text=f"{acc.name}", font=("Segoe UI", 10, "bold")).pack(anchor=tk.W)
262+
tb.Label(info_frame, text=f"{acc.city} | ${acc.price}{acc.rating}", font=("Segoe UI", 9)).pack(anchor=tk.W)
263+
264+
btn_frame = tk.Frame(info_frame)
265+
btn_frame.pack(anchor=tk.W, pady=2)
266+
tb.Button(btn_frame, text="★ Favorite", bootstyle="warning", command=lambda a=acc: self.add_to_favorites(a)).pack(side=tk.LEFT, padx=2)
267+
tb.Button(btn_frame, text="↗ Open", bootstyle="info", command=lambda u=acc.url: webbrowser.open_new_tab(u)).pack(side=tk.LEFT, padx=2)
268+
269+
container_widget.configure(state="normal")
270+
container_widget.window_create("end", window=frame)
271+
container_widget.insert("end", "\n\n")
272+
container_widget.configure(state="disabled")
273+
274+
# Async image loading
275+
def load_cover_async():
276+
img = load_cover(acc.image_url)
277+
if img:
278+
def set_img():
279+
if art_label.winfo_exists():
280+
art_label.configure(image=img)
281+
art_label.image = img
282+
container_widget.after(0, set_img)
283+
threading.Thread(target=load_cover_async, daemon=True).start()
284+
285+
# ---------------- PAGINATION ---------------- #
286+
def next_page(self):
287+
if self.current_page * RESULTS_PER_PAGE < len(self.results):
288+
self.current_page += 1
289+
self.display_page()
290+
291+
def prev_page(self):
292+
if self.current_page > 1:
293+
self.current_page -= 1
294+
self.display_page()
295+
296+
# ---------------- FAVORITES ---------------- #
297+
def show_favorites(self):
298+
fav_win = tb.Toplevel(self.app)
299+
fav_win.title("Favorites")
300+
fav_win.geometry("700x500")
301+
fav_text = ScrolledText(fav_win)
302+
fav_text.pack(fill=tk.BOTH, expand=True)
303+
for a in self.favorites:
304+
self.insert_accommodation(fav_text.text, a)
305+
fav_text.text.configure(state="disabled")
306+
307+
# ---------------- RUN ---------------- #
308+
if __name__ == "__main__":
309+
AccommodationEngine()

0 commit comments

Comments
 (0)