33import tkinter as tk
44from tkinter import messagebox
55from dataclasses import dataclass
6- from typing import List , Tuple , Optional , Dict
6+ from typing import List , Tuple , Dict , Optional
77import requests
88import io
99from PIL import Image , ImageTk # pip install pillow
2626@dataclass
2727class Movie :
2828 title : str
29+ imdb_id : str
2930 url : str
30- display_url : str
3131 description : str
3232 poster_url : str
33+ genres : List [str ] = None
34+ director : str = ""
35+ actors : List [str ] = None
36+ rating : float = 0.0
3337
34- # ---------------- SEARCH LOGIC ---------------- #
38+ # ---------------- SEARCH / FETCH ---------------- #
3539def fetch_movies (query : str ) -> List [Movie ]:
3640 """Fetch movies from OMDb API matching the query."""
3741 movies : List [Movie ] = []
@@ -51,13 +55,19 @@ def fetch_movies(query: str) -> List[Movie]:
5155 timeout = 10
5256 ).json ()
5357
58+ genres = detail_resp .get ("Genre" , "" )
59+ actors = detail_resp .get ("Actors" , "" )
5460 movies .append (
5561 Movie (
5662 title = detail_resp .get ("Title" , "Unknown" ),
63+ imdb_id = imdb_id ,
5764 url = f"https://www.imdb.com/title/{ imdb_id } /" ,
58- display_url = f"imdb.com › { imdb_id } " ,
5965 description = detail_resp .get ("Plot" , "" ),
60- poster_url = detail_resp .get ("Poster" , "" )
66+ poster_url = detail_resp .get ("Poster" , "" ),
67+ genres = [g .strip () for g in genres .split ("," )] if genres else [],
68+ director = detail_resp .get ("Director" , "" ),
69+ actors = [a .strip () for a in actors .split ("," )] if actors else [],
70+ rating = float (detail_resp .get ("imdbRating" , 0.0 )) if detail_resp .get ("imdbRating" ) != "N/A" else 0.0
6171 )
6272 )
6373 except requests .RequestException as e :
@@ -66,9 +76,27 @@ def fetch_movies(query: str) -> List[Movie]:
6676 messagebox .showerror ("Error" , str (e ))
6777 return movies
6878
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 ]
79+ # ---------------- RECOMMENDATION ENGINE ---------------- #
80+ def recommend_movies (query : str , candidates : List [Movie ], top_n = RESULTS_PER_PAGE ) -> List [Tuple [Movie , float ]]:
81+ """
82+ Simple content-based recommendation:
83+ Scores movies by overlap with query in title, description, and genres
84+ """
85+ query_tokens = set (query .lower ().split ())
86+ recommendations : List [Tuple [Movie , float ]] = []
87+
88+ for movie in candidates :
89+ text_to_match = " " .join ([
90+ movie .title .lower (),
91+ movie .description .lower (),
92+ " " .join (movie .genres or [])
93+ ])
94+ score = sum (1 for token in query_tokens if token in text_to_match )
95+ recommendations .append ((movie , score ))
96+
97+ # Sort descending by score
98+ recommendations .sort (key = lambda x : x [1 ], reverse = True )
99+ return recommendations [:top_n ]
72100
73101# ---------------- UI HELPERS ---------------- #
74102def open_url (url : str ):
@@ -104,14 +132,14 @@ def display_page():
104132 update_pagination ()
105133 return
106134
107- for idx , (movie , _ ) in enumerate (page_results ):
108- # Movie Title
135+ for idx , (movie , score ) in enumerate (page_results ):
136+ # Title
109137 text .insert ("end" , f"{ movie .title } \n " , f"title_{ idx } " )
110138 text .tag_config (f"title_{ idx } " , foreground = "#1a0dab" , font = ("Segoe UI" , 14 , "bold" ))
111139 text .tag_bind (f"title_{ idx } " , "<Double-Button-1>" , lambda e , url = movie .url : open_url (url ))
112140
113- # Display URL
114- text .insert ("end" , f"{ movie .display_url } \n " , f"url_{ idx } " )
141+ # Display URL and IMDB rating
142+ text .insert ("end" , f"{ movie .url } | Rating: { movie . rating } \n " , f"url_{ idx } " )
115143 text .tag_config (f"url_{ idx } " , foreground = "#006621" , font = ("Segoe UI" , 10 ))
116144
117145 # Poster
@@ -120,8 +148,9 @@ def display_page():
120148 text .image_create ("end" , image = poster )
121149 text .insert ("end" , "\n " )
122150
123- # Description
124- text .insert ("end" , f"{ movie .description } \n \n " )
151+ # Description & genres
152+ genres = ", " .join (movie .genres or [])
153+ text .insert ("end" , f"{ movie .description } \n Genres: { genres } \n \n " )
125154
126155 text .configure (state = "disabled" )
127156 update_pagination ()
@@ -149,24 +178,24 @@ def update_pagination():
149178def perform_search ():
150179 query = query_entry .get ().strip ()
151180 if not query :
152- messagebox .showwarning ("Input Required" , "Enter a movie title." )
181+ messagebox .showwarning ("Input Required" , "Enter a movie title or keywords ." )
153182 return
154183 threading .Thread (target = search_thread , args = (query ,), daemon = True ).start ()
155184
156185def search_thread (query : str ):
157186 global all_ranked_movies , current_page
158187 current_page = 1
159- all_movies = fetch_movies (query )
160- all_ranked_movies = rank_movies (query , all_movies )
188+ candidates = fetch_movies (query )
189+ all_ranked_movies = recommend_movies (query , candidates , top_n = 50 ) # get top 50 recommendations
161190 display_page ()
162191
163192# ---------------- UI SETUP ---------------- #
164- app = tb .Window (title = "Live Movie Search " , themename = "flatly" , size = (980 , 720 ), resizable = (True , True ))
193+ app = tb .Window (title = "Movie Recommendation Engine " , themename = "flatly" , size = (980 , 720 ), resizable = (True , True ))
165194
166195# Top frame
167196top = tb .Frame (app , padding = 15 )
168197top .pack (fill = tk .X )
169- tb .Label (top , text = "Search Movies (Live) " , font = ("Segoe UI" , 16 , "bold" )).pack (anchor = tk .W )
198+ tb .Label (top , text = "Movie Recommendation Engine " , font = ("Segoe UI" , 16 , "bold" )).pack (anchor = tk .W )
170199
171200query_entry = tb .Entry (top , font = ("Segoe UI" , 12 ))
172201query_entry .pack (fill = tk .X , pady = 8 )
0 commit comments