Skip to content

Commit 074096e

Browse files
authored
Create Movie-Recommendation-Engine.py
1 parent 0b3b49a commit 074096e

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
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, Optional, Dict
7+
import requests
8+
import io
9+
from PIL import Image, ImageTk # pip install pillow
10+
11+
import ttkbootstrap as tb
12+
from ttkbootstrap.constants import *
13+
from ttkbootstrap.widgets.scrolled import ScrolledText
14+
15+
# ---------------- CONFIG ---------------- #
16+
RESULTS_PER_PAGE = 6
17+
OMDB_API_KEY = "YOUR_OMDB_API_KEY" # ← replace with your OMDb API key
18+
OMDB_SEARCH_URL = "http://www.omdbapi.com/"
19+
20+
# ---------------- GLOBAL STATE ---------------- #
21+
all_ranked_movies: List[Tuple["Movie", float]] = []
22+
current_page = 1
23+
poster_cache: Dict[str, ImageTk.PhotoImage] = {}
24+
25+
# ---------------- DATA STRUCTURE ---------------- #
26+
@dataclass
27+
class Movie:
28+
title: str
29+
url: str
30+
display_url: str
31+
description: str
32+
poster_url: str
33+
34+
# ---------------- SEARCH LOGIC ---------------- #
35+
def fetch_movies(query: str) -> List[Movie]:
36+
"""Fetch movies from OMDb API matching the query."""
37+
movies: List[Movie] = []
38+
try:
39+
params = {"apikey": OMDB_API_KEY, "s": query, "type": "movie"}
40+
resp = requests.get(OMDB_SEARCH_URL, params=params, timeout=10)
41+
data = resp.json()
42+
43+
if data.get("Response") != "True":
44+
return []
45+
46+
for item in data.get("Search", []):
47+
imdb_id = item.get("imdbID")
48+
detail_resp = requests.get(
49+
OMDB_SEARCH_URL,
50+
params={"apikey": OMDB_API_KEY, "i": imdb_id, "plot": "short"},
51+
timeout=10
52+
).json()
53+
54+
movies.append(
55+
Movie(
56+
title=detail_resp.get("Title", "Unknown"),
57+
url=f"https://www.imdb.com/title/{imdb_id}/",
58+
display_url=f"imdb.com › {imdb_id}",
59+
description=detail_resp.get("Plot", ""),
60+
poster_url=detail_resp.get("Poster", "")
61+
)
62+
)
63+
except requests.RequestException as e:
64+
messagebox.showerror("API Error", f"Network error: {e}")
65+
except Exception as e:
66+
messagebox.showerror("Error", str(e))
67+
return movies
68+
69+
def rank_movies(query: str, movies: List[Movie]) -> List[Tuple[Movie, float]]:
70+
"""Placeholder ranking logic; can implement TF-IDF or other metrics."""
71+
return [(m, 0.0) for m in movies]
72+
73+
# ---------------- UI HELPERS ---------------- #
74+
def open_url(url: str):
75+
webbrowser.open_new_tab(url)
76+
77+
def load_image(url: str, size=(100, 150)) -> Optional[ImageTk.PhotoImage]:
78+
"""Load poster image from URL, with caching."""
79+
if not url or url == "N/A":
80+
return None
81+
if url in poster_cache:
82+
return poster_cache[url]
83+
try:
84+
resp = requests.get(url, timeout=10)
85+
img = Image.open(io.BytesIO(resp.content))
86+
img = img.resize(size, Image.ANTIALIAS)
87+
photo = ImageTk.PhotoImage(img)
88+
poster_cache[url] = photo
89+
return photo
90+
except Exception:
91+
return None
92+
93+
def display_page():
94+
text.configure(state="normal")
95+
text.delete("1.0", "end")
96+
97+
start = (current_page - 1) * RESULTS_PER_PAGE
98+
end = start + RESULTS_PER_PAGE
99+
page_results = all_ranked_movies[start:end]
100+
101+
if not page_results:
102+
text.insert("end", "No results found.\n")
103+
text.configure(state="disabled")
104+
update_pagination()
105+
return
106+
107+
for idx, (movie, _) in enumerate(page_results):
108+
# Movie Title
109+
text.insert("end", f"{movie.title}\n", f"title_{idx}")
110+
text.tag_config(f"title_{idx}", foreground="#1a0dab", font=("Segoe UI", 14, "bold"))
111+
text.tag_bind(f"title_{idx}", "<Double-Button-1>", lambda e, url=movie.url: open_url(url))
112+
113+
# Display URL
114+
text.insert("end", f"{movie.display_url}\n", f"url_{idx}")
115+
text.tag_config(f"url_{idx}", foreground="#006621", font=("Segoe UI", 10))
116+
117+
# Poster
118+
poster = load_image(movie.poster_url)
119+
if poster:
120+
text.image_create("end", image=poster)
121+
text.insert("end", "\n")
122+
123+
# Description
124+
text.insert("end", f"{movie.description}\n\n")
125+
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_movies):
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_pages = max(1, (len(all_ranked_movies) - 1) // RESULTS_PER_PAGE + 1)
144+
page_label.config(text=f"Page {current_page} of {total_pages}")
145+
prev_btn.config(state=DISABLED if current_page == 1 else NORMAL)
146+
next_btn.config(state=DISABLED if current_page == total_pages else NORMAL)
147+
148+
# ---------------- SEARCH ---------------- #
149+
def perform_search():
150+
query = query_entry.get().strip()
151+
if not query:
152+
messagebox.showwarning("Input Required", "Enter a movie title.")
153+
return
154+
threading.Thread(target=search_thread, args=(query,), daemon=True).start()
155+
156+
def search_thread(query: str):
157+
global all_ranked_movies, current_page
158+
current_page = 1
159+
all_movies = fetch_movies(query)
160+
all_ranked_movies = rank_movies(query, all_movies)
161+
display_page()
162+
163+
# ---------------- UI SETUP ---------------- #
164+
app = tb.Window(title="Live Movie Search", themename="flatly", size=(980, 720), resizable=(True, True))
165+
166+
# Top frame
167+
top = tb.Frame(app, padding=15)
168+
top.pack(fill=tk.X)
169+
tb.Label(top, text="Search Movies (Live)", font=("Segoe UI", 16, "bold")).pack(anchor=tk.W)
170+
171+
query_entry = tb.Entry(top, font=("Segoe UI", 12))
172+
query_entry.pack(fill=tk.X, pady=8)
173+
query_entry.bind("<Return>", lambda e: perform_search())
174+
175+
tb.Button(top, text="Search", bootstyle="primary", command=perform_search).pack(anchor=tk.E)
176+
177+
# Results frame
178+
result_frame = tb.Frame(app, padding=(15, 5))
179+
result_frame.pack(fill=tk.BOTH, expand=True)
180+
181+
result_box = ScrolledText(result_frame, autohide=True)
182+
result_box.pack(fill=tk.BOTH, expand=True)
183+
text = result_box.text
184+
text.configure(state="disabled", wrap="word")
185+
186+
# Navigation
187+
nav = tb.Frame(app, padding=10)
188+
nav.pack(fill=tk.X)
189+
190+
prev_btn = tb.Button(nav, text="← Prev", bootstyle="secondary", command=prev_page)
191+
prev_btn.pack(side=tk.LEFT)
192+
193+
page_label = tb.Label(nav, text="Page 1", font=("Segoe UI", 10))
194+
page_label.pack(side=tk.LEFT, padx=10)
195+
196+
next_btn = tb.Button(nav, text="Next →", bootstyle="secondary", command=next_page)
197+
next_btn.pack(side=tk.LEFT)
198+
199+
app.mainloop()

0 commit comments

Comments
 (0)