Skip to content

Commit b83454b

Browse files
authored
Create Book-Recommendation-Engine.py
1 parent 98dd778 commit b83454b

1 file changed

Lines changed: 225 additions & 0 deletions

File tree

Lines changed: 225 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,225 @@
1+
import threading
2+
import webbrowser
3+
import tkinter as tk
4+
from tkinter import messagebox
5+
from dataclasses import dataclass
6+
from typing import List, Tuple, Dict
7+
import requests
8+
import io
9+
import json
10+
import os
11+
12+
from PIL import Image, ImageTk
13+
14+
from sklearn.feature_extraction.text import TfidfVectorizer
15+
from sklearn.metrics.pairwise import cosine_similarity
16+
17+
import ttkbootstrap as tb
18+
from ttkbootstrap.constants import *
19+
from ttkbootstrap.widgets.scrolled import ScrolledText
20+
21+
# ---------------- CONFIG ---------------- #
22+
RESULTS_PER_PAGE = 6
23+
OPENLIBRARY_SEARCH_URL = "https://openlibrary.org/search.json"
24+
25+
# ---------------- GLOBAL STATE ---------------- #
26+
all_ranked_books: List[Tuple["Book", float]] = []
27+
current_page = 1
28+
cover_cache: Dict[str, ImageTk.PhotoImage] = {}
29+
30+
# ---------------- DATA STRUCTURE ---------------- #
31+
@dataclass(frozen=True)
32+
class Book:
33+
title: str
34+
authors: List[str]
35+
description: str
36+
url: str
37+
cover_url: str = ""
38+
publish_year: int = 0
39+
40+
def text_blob(self, mode: str):
41+
if mode == "Author":
42+
return " ".join(self.authors)
43+
return f"{self.title} {' '.join(self.authors)} {self.description}"
44+
45+
46+
# ---------------- SEARCH / FETCH ---------------- #
47+
def fetch_books(query: str, limit=40) -> List[Book]:
48+
books = []
49+
params = {"q": query, "limit": limit}
50+
resp = requests.get(OPENLIBRARY_SEARCH_URL, params=params, timeout=10).json()
51+
for item in resp.get("docs", []):
52+
title = item.get("title", "")
53+
authors = item.get("author_name", [])
54+
key = item.get("key", "")
55+
url = f"https://openlibrary.org{key}" if key else ""
56+
cover_id = item.get("cover_i", None)
57+
cover_url = f"https://covers.openlibrary.org/b/id/{cover_id}-M.jpg" if cover_id else ""
58+
description = item.get("first_sentence", {}).get("value", "") if item.get("first_sentence") else ""
59+
publish_year = item.get("first_publish_year", 0)
60+
books.append(Book(title=title, authors=authors, description=description, url=url, cover_url=cover_url, publish_year=publish_year))
61+
return books
62+
63+
# ---------------- RECOMMENDATION ENGINE ---------------- #
64+
def recommend_books(query: str, candidates: List[Book], mode: str) -> List[Tuple[Book, float]]:
65+
docs = [b.text_blob(mode) for b in candidates]
66+
vectorizer = TfidfVectorizer(stop_words="english")
67+
tfidf = vectorizer.fit_transform(docs + [query])
68+
scores = cosine_similarity(tfidf[-1], tfidf[:-1]).flatten()
69+
ranked = list(zip(candidates, scores))
70+
ranked.sort(key=lambda x: x[1], reverse=True)
71+
return ranked
72+
73+
# ---------------- UI HELPERS ---------------- #
74+
def open_url(url: str):
75+
webbrowser.open_new_tab(url)
76+
77+
def load_cover(url: str, size=(120, 180)):
78+
if not url:
79+
return None
80+
if url in cover_cache:
81+
return cover_cache[url]
82+
try:
83+
img = Image.open(io.BytesIO(requests.get(url).content)).resize(size)
84+
photo = ImageTk.PhotoImage(img)
85+
cover_cache[url] = photo
86+
return photo
87+
except:
88+
return None
89+
90+
# ---------------- DISPLAY MAIN ---------------- #
91+
def insert_book(container_widget, book: Book):
92+
frame = tk.Frame(container_widget)
93+
art_label = tk.Label(frame)
94+
art_label.pack(side=tk.LEFT)
95+
96+
info_frame = tk.Frame(frame)
97+
info_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=5, pady=5)
98+
tb.Label(info_frame, text=f"{book.title}", font=("Segoe UI", 10, "bold")).pack(anchor=tk.W)
99+
tb.Label(info_frame, text=f"By {', '.join(book.authors)} | Year: {book.publish_year}", font=("Segoe UI", 9)).pack(anchor=tk.W)
100+
btn_frame = tk.Frame(info_frame)
101+
btn_frame.pack(anchor=tk.W, pady=2)
102+
tb.Button(btn_frame, text="↗ Open", bootstyle="info", command=lambda u=book.url: open_url(u)).pack(side=tk.LEFT, padx=2)
103+
104+
container_widget.configure(state="normal")
105+
container_widget.window_create("end", window=frame)
106+
container_widget.insert("end", "\n\n")
107+
container_widget.configure(state="disabled")
108+
109+
def async_load():
110+
img = load_cover(book.cover_url)
111+
if img:
112+
def set_img():
113+
if art_label.winfo_exists():
114+
art_label.configure(image=img)
115+
art_label.image = img
116+
container_widget.after(0, set_img)
117+
threading.Thread(target=async_load, daemon=True).start()
118+
119+
def display_page():
120+
text.configure(state="normal")
121+
text.delete("1.0", "end")
122+
start = (current_page - 1) * RESULTS_PER_PAGE
123+
end = start + RESULTS_PER_PAGE
124+
for book, score in all_ranked_books[start:end]:
125+
insert_book(text, book)
126+
text.configure(state="disabled")
127+
update_pagination()
128+
129+
# ---------------- PAGINATION ---------------- #
130+
def next_page():
131+
global current_page
132+
if current_page * RESULTS_PER_PAGE < len(all_ranked_books):
133+
current_page += 1
134+
display_page()
135+
136+
def prev_page():
137+
global current_page
138+
if current_page > 1:
139+
current_page -= 1
140+
display_page()
141+
142+
def update_pagination():
143+
total = max(1, (len(all_ranked_books) - 1) // RESULTS_PER_PAGE + 1)
144+
page_label.config(text=f"Page {current_page} of {total}")
145+
146+
# ---------------- SEARCH ---------------- #
147+
def get_selected_modes():
148+
selected = [name for name, var in mode_vars.items() if var.get() == 1]
149+
if not selected:
150+
return ["All"]
151+
return selected
152+
153+
def perform_search():
154+
query = query_entry.get().strip()
155+
if not query:
156+
return
157+
threading.Thread(target=search_thread, args=(query,), daemon=True).start()
158+
159+
def search_thread(query: str):
160+
selected_modes = get_selected_modes()
161+
books = fetch_books(query)
162+
ranked_list = []
163+
for mode in selected_modes:
164+
ranked = recommend_books(query, books, mode)
165+
ranked_list.extend(ranked)
166+
ranked_list.sort(key=lambda x: x[1], reverse=True)
167+
seen = set()
168+
final_list = []
169+
for book, score in ranked_list:
170+
key = book.title + "".join(book.authors)
171+
if key not in seen:
172+
seen.add(key)
173+
final_list.append((book, score))
174+
def update_ui():
175+
global all_ranked_books, current_page
176+
current_page = 1
177+
all_ranked_books = final_list
178+
display_page()
179+
app.after(0, update_ui)
180+
181+
# ---------------- UI SETUP ---------------- #
182+
app = tb.Window(title="Book Recommendation Engine", themename="darkly", size=(1000, 680), resizable=(True, True))
183+
184+
top = tb.Frame(app, padding=15)
185+
top.pack(fill=tk.X)
186+
187+
tb.Label(top, text="📚 Book Recommendation Engine", font=("Segoe UI", 18, "bold")).pack(anchor=tk.W)
188+
189+
query_entry = tb.Entry(top, font=("Segoe UI", 13))
190+
query_entry.pack(fill=tk.X, pady=6)
191+
query_entry.bind("<Return>", lambda e: perform_search())
192+
193+
# Mode selection
194+
mode_vars = {"All": tk.IntVar(value=1), "Author": tk.IntVar(value=0)}
195+
def update_modes(changed):
196+
if changed == "All" and mode_vars["All"].get() == 1:
197+
mode_vars["Author"].set(0)
198+
elif changed == "Author" and mode_vars["Author"].get() == 1:
199+
mode_vars["All"].set(0)
200+
elif all(var.get() == 0 for var in mode_vars.values()):
201+
mode_vars["All"].set(1)
202+
mode_button_frame = tb.Frame(top)
203+
mode_button_frame.pack(fill=tk.X, pady=5)
204+
for name, var in mode_vars.items():
205+
tb.Checkbutton(mode_button_frame, text=name, variable=var, bootstyle="info", command=lambda n=name: update_modes(n)).pack(side=tk.LEFT, padx=5)
206+
tb.Label(mode_button_frame, text=" " * 5).pack(side=tk.LEFT)
207+
tb.Button(mode_button_frame, text="Search", bootstyle="primary", command=perform_search).pack(side=tk.LEFT, padx=5)
208+
209+
# Results
210+
result_frame = tb.Frame(app)
211+
result_frame.pack(fill=tk.BOTH, expand=True)
212+
result_box = ScrolledText(result_frame)
213+
result_box.pack(fill=tk.BOTH, expand=True)
214+
text = result_box.text
215+
text.configure(state="disabled", wrap="word")
216+
217+
# Pagination
218+
nav = tb.Frame(app, padding=10)
219+
nav.pack()
220+
tb.Button(nav, text="← Prev", command=prev_page).pack(side=tk.LEFT)
221+
page_label = tb.Label(nav, text="Page 1")
222+
page_label.pack(side=tk.LEFT, padx=10)
223+
tb.Button(nav, text="Next →", command=next_page).pack(side=tk.LEFT)
224+
225+
app.mainloop()

0 commit comments

Comments
 (0)