Merge branch 'development'
Browse files- .env.example +2 -0
- components/gradio_ui.py +37 -103
- components/imdb_poster.py +17 -0
- components/tmdb_api.py +17 -19
- models/pydantic_schemas.py +1 -1
- models/recommendation_engine.py +4 -16
- tmdb_api.py +54 -0
.env.example
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
OPENAI_API_KEY=
|
| 2 |
+
TMDB_API_KEY=
|
components/gradio_ui.py
CHANGED
|
@@ -1,116 +1,50 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
def create_interface(engine):
|
| 4 |
-
def
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
if isinstance(result, tuple) and len(result) >= 1:
|
| 8 |
-
return result[0]
|
| 9 |
-
else:
|
| 10 |
-
return str(result)
|
| 11 |
-
except Exception as e:
|
| 12 |
-
return f"❌ Error: {str(e)}"
|
| 13 |
|
| 14 |
-
def get_thumbnails_html(query):
|
| 15 |
try:
|
| 16 |
-
result = engine.get_recommendations(
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
|
|
|
|
|
|
| 20 |
|
| 21 |
-
|
| 22 |
-
thumbnails_html.append("""
|
| 23 |
-
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; max-height: 600px; overflow-y: auto;">
|
| 24 |
-
""")
|
| 25 |
|
| 26 |
-
|
| 27 |
-
<div style="grid-column: 1 / -1; text-align: center; padding: 20px; color: #666;">
|
| 28 |
-
Thumbnails will appear here when poster URLs are available
|
| 29 |
-
</div>
|
| 30 |
-
""")
|
| 31 |
-
|
| 32 |
-
thumbnails_html.append("</div>")
|
| 33 |
-
return "".join(thumbnails_html)
|
| 34 |
-
|
| 35 |
-
except Exception as e:
|
| 36 |
-
return f"<div style='color: red; padding: 20px;'>❌ Error: {str(e)}</div>"
|
| 37 |
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
try:
|
| 41 |
-
formatted_results, df_results = engine.get_recommendations(query)
|
| 42 |
-
|
| 43 |
-
html_parts = []
|
| 44 |
-
html_parts.append("""
|
| 45 |
-
<div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 15px; padding: 20px; max-height: 600px; overflow-y: auto; background: #f8f9fa; border-radius: 8px;">
|
| 46 |
-
""")
|
| 47 |
-
|
| 48 |
-
for i in range(10):
|
| 49 |
-
html_parts.append(f"""
|
| 50 |
-
<div style="position: relative; border-radius: 8px; overflow: hidden; box-shadow: 0 2px 8px rgba(0,0,0,0.1); transition: transform 0.2s; cursor: pointer;"
|
| 51 |
-
onmouseover="this.style.transform='scale(1.05)'"
|
| 52 |
-
onmouseout="this.style.transform='scale(1)'">
|
| 53 |
-
<div style="width: 100%; height: 200px; background: #ddd; display: flex; align-items: center; justify-content: center; color: #666; font-size: 12px;">
|
| 54 |
-
Poster {i+1}
|
| 55 |
-
</div>
|
| 56 |
-
<div style="position: absolute; bottom: 0; left: 0; right: 0; background: linear-gradient(transparent, rgba(0,0,0,0.7)); color: white; padding: 8px; font-size: 12px; text-align: center;">
|
| 57 |
-
Movie Title {i+1}
|
| 58 |
-
</div>
|
| 59 |
-
</div>
|
| 60 |
-
""")
|
| 61 |
-
|
| 62 |
-
html_parts.append("</div>")
|
| 63 |
-
return "".join(html_parts)
|
| 64 |
-
|
| 65 |
-
except Exception as e:
|
| 66 |
-
return f"<div style='color: red; padding: 20px;'>❌ Error: {str(e)}</div>"
|
| 67 |
-
|
| 68 |
-
with gr.Blocks(
|
| 69 |
-
theme=gr.themes.Soft(),
|
| 70 |
-
title="TV-Series and Movie Recommend",
|
| 71 |
-
css="""
|
| 72 |
-
.gradio-container {
|
| 73 |
-
max-width: 1200px !important;
|
| 74 |
-
}
|
| 75 |
-
"""
|
| 76 |
-
) as demo:
|
| 77 |
-
gr.Markdown("# 🎬 TV-Series and Movie Recommend")
|
| 78 |
|
| 79 |
-
|
| 80 |
-
with gr.Column(scale=1):
|
| 81 |
-
query_input = gr.Textbox(
|
| 82 |
-
label="What you want to watch?",
|
| 83 |
-
placeholder="Define your preferences as detailed as possible.",
|
| 84 |
-
lines=3,
|
| 85 |
-
)
|
| 86 |
|
| 87 |
-
|
|
|
|
|
|
|
| 88 |
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
|
| 96 |
-
)
|
| 97 |
-
|
| 98 |
-
with gr.Column(scale=1):
|
| 99 |
-
thumbnails_display = gr.HTML(
|
| 100 |
-
label="Movie Posters",
|
| 101 |
-
value="<div style='text-align: center; padding: 40px; color: #666;'>Movie thumbnails will appear here</div>"
|
| 102 |
-
)
|
| 103 |
|
| 104 |
-
|
| 105 |
-
fn=get_recommendations_text,
|
| 106 |
-
inputs=[query_input],
|
| 107 |
-
outputs=[results_text],
|
| 108 |
-
)
|
| 109 |
-
|
| 110 |
-
search_btn.click(
|
| 111 |
-
fn=get_thumbnails_from_results,
|
| 112 |
-
inputs=[query_input],
|
| 113 |
-
outputs=[thumbnails_display],
|
| 114 |
-
)
|
| 115 |
|
| 116 |
-
return demo
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
from components.imdb_poster import get_imdb_poster
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_multiple_imdb_posters(imdb_ids):
|
| 6 |
+
posters = []
|
| 7 |
+
for imdb_id in imdb_ids:
|
| 8 |
+
poster_url = get_imdb_poster(imdb_id)
|
| 9 |
+
print(f"IMDB ID: {imdb_id}, Poster URL: {poster_url}")
|
| 10 |
+
posters.append({"tconst": imdb_id, "poster_url": poster_url})
|
| 11 |
+
return posters
|
| 12 |
+
|
| 13 |
|
| 14 |
def create_interface(engine):
|
| 15 |
+
def chat_function(message, history):
|
| 16 |
+
if not message:
|
| 17 |
+
return history, "", []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
|
|
|
| 19 |
try:
|
| 20 |
+
result = engine.get_recommendations(message)
|
| 21 |
+
imdb_ids = []
|
| 22 |
+
df = result[1]
|
| 23 |
+
if isinstance(result, tuple) and len(result) > 1:
|
| 24 |
+
if hasattr(result[1], "columns") and "ImdbId" in result[1].columns:
|
| 25 |
+
imdb_ids = result[1]["ImdbId"].tolist()
|
| 26 |
|
| 27 |
+
posters = get_multiple_imdb_posters(imdb_ids)
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
+
thumbnails = [p["poster_url"] for p in posters if p["poster_url"]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
+
response_text = result[0] if isinstance(result, tuple) else str(result)
|
| 32 |
+
history.append([message, response_text])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 33 |
|
| 34 |
+
return history, "", thumbnails
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
|
| 36 |
+
except Exception as e:
|
| 37 |
+
history.append([message, f"❌ Error: {str(e)}"])
|
| 38 |
+
return history, "", []
|
| 39 |
|
| 40 |
+
with gr.Blocks() as demo:
|
| 41 |
+
with gr.Column():
|
| 42 |
+
chatbot = gr.Chatbot(height=600)
|
| 43 |
+
gallery = gr.Gallery(
|
| 44 |
+
label="Posters", show_label=False, columns=5, height=400
|
| 45 |
+
)
|
| 46 |
+
msg = gr.Textbox(placeholder="Enter your query", scale=1)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
|
| 48 |
+
msg.submit(chat_function, [msg, chatbot], [chatbot, msg, gallery])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 49 |
|
| 50 |
+
return demo
|
components/imdb_poster.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from bs4 import BeautifulSoup
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def get_imdb_poster(imdb_id):
|
| 6 |
+
url = f"https://www.imdb.com/title/{imdb_id}/"
|
| 7 |
+
headers = {"User-Agent": "Mozilla/5.0"}
|
| 8 |
+
r = requests.get(url, headers=headers)
|
| 9 |
+
soup = BeautifulSoup(r.text, "html.parser")
|
| 10 |
+
|
| 11 |
+
poster_div = soup.find("div", class_="ipc-poster")
|
| 12 |
+
if poster_div:
|
| 13 |
+
img_tag = poster_div.find("img")
|
| 14 |
+
if img_tag and img_tag.get("src"):
|
| 15 |
+
return img_tag["src"]
|
| 16 |
+
|
| 17 |
+
return None
|
components/tmdb_api.py
CHANGED
|
@@ -1,54 +1,52 @@
|
|
| 1 |
import requests
|
| 2 |
from config import Config
|
| 3 |
|
|
|
|
| 4 |
class TMDBApi:
|
| 5 |
def __init__(self):
|
| 6 |
self.config = Config()
|
| 7 |
self.base_url = self.config.TMDB_BASE_URL
|
| 8 |
self.api_key = self.config.TMDB_API_KEY
|
| 9 |
self.image_base_url = self.config.TMDB_IMAGE_BASE_URL
|
| 10 |
-
|
| 11 |
def get_poster_by_imdb_id(self, imdb_id: str):
|
| 12 |
try:
|
| 13 |
-
if not imdb_id.startswith(
|
| 14 |
imdb_id = f"tt{imdb_id}"
|
| 15 |
-
|
| 16 |
endpoint = f"{self.base_url}/find/{imdb_id}"
|
| 17 |
-
params = {
|
| 18 |
-
|
| 19 |
-
"external_source": "tconst"
|
| 20 |
-
}
|
| 21 |
-
|
| 22 |
response = requests.get(endpoint, params=params)
|
| 23 |
response.raise_for_status()
|
| 24 |
-
|
| 25 |
data = response.json()
|
| 26 |
-
|
| 27 |
poster_path = None
|
| 28 |
if data.get("movie_results"):
|
| 29 |
poster_path = data["movie_results"][0].get("poster_path")
|
| 30 |
elif data.get("tv_results"):
|
| 31 |
poster_path = data["tv_results"][0].get("poster_path")
|
| 32 |
-
|
| 33 |
if poster_path:
|
| 34 |
return f"{self.image_base_url}{poster_path}"
|
| 35 |
return None
|
| 36 |
-
|
| 37 |
except Exception as e:
|
| 38 |
print(f"❌ TMDB API Error for IMDB ID {imdb_id}: {str(e)}")
|
| 39 |
return None
|
| 40 |
-
|
| 41 |
def get_multiple_posters_by_imdb(self, items: list):
|
| 42 |
results = []
|
| 43 |
for item in items:
|
| 44 |
-
imdb_id = item.get(
|
| 45 |
-
|
| 46 |
if imdb_id:
|
| 47 |
poster_url = self.get_poster_by_imdb_id(imdb_id)
|
| 48 |
-
item[
|
| 49 |
else:
|
| 50 |
-
item[
|
| 51 |
-
|
| 52 |
results.append(item)
|
| 53 |
-
|
| 54 |
return results
|
|
|
|
| 1 |
import requests
|
| 2 |
from config import Config
|
| 3 |
|
| 4 |
+
|
| 5 |
class TMDBApi:
|
| 6 |
def __init__(self):
|
| 7 |
self.config = Config()
|
| 8 |
self.base_url = self.config.TMDB_BASE_URL
|
| 9 |
self.api_key = self.config.TMDB_API_KEY
|
| 10 |
self.image_base_url = self.config.TMDB_IMAGE_BASE_URL
|
| 11 |
+
|
| 12 |
def get_poster_by_imdb_id(self, imdb_id: str):
|
| 13 |
try:
|
| 14 |
+
if not imdb_id.startswith("tt"):
|
| 15 |
imdb_id = f"tt{imdb_id}"
|
| 16 |
+
|
| 17 |
endpoint = f"{self.base_url}/find/{imdb_id}"
|
| 18 |
+
params = {"api_key": self.api_key, "external_source": "tconst"}
|
| 19 |
+
|
|
|
|
|
|
|
|
|
|
| 20 |
response = requests.get(endpoint, params=params)
|
| 21 |
response.raise_for_status()
|
| 22 |
+
|
| 23 |
data = response.json()
|
| 24 |
+
|
| 25 |
poster_path = None
|
| 26 |
if data.get("movie_results"):
|
| 27 |
poster_path = data["movie_results"][0].get("poster_path")
|
| 28 |
elif data.get("tv_results"):
|
| 29 |
poster_path = data["tv_results"][0].get("poster_path")
|
| 30 |
+
|
| 31 |
if poster_path:
|
| 32 |
return f"{self.image_base_url}{poster_path}"
|
| 33 |
return None
|
| 34 |
+
|
| 35 |
except Exception as e:
|
| 36 |
print(f"❌ TMDB API Error for IMDB ID {imdb_id}: {str(e)}")
|
| 37 |
return None
|
| 38 |
+
|
| 39 |
def get_multiple_posters_by_imdb(self, items: list):
|
| 40 |
results = []
|
| 41 |
for item in items:
|
| 42 |
+
imdb_id = item.get("tconst")
|
| 43 |
+
|
| 44 |
if imdb_id:
|
| 45 |
poster_url = self.get_poster_by_imdb_id(imdb_id)
|
| 46 |
+
item["poster_url"] = poster_url
|
| 47 |
else:
|
| 48 |
+
item["poster_url"] = None
|
| 49 |
+
|
| 50 |
results.append(item)
|
| 51 |
+
|
| 52 |
return results
|
models/pydantic_schemas.py
CHANGED
|
@@ -16,7 +16,7 @@ class Features(BaseModel):
|
|
| 16 |
themes: list[str] = Field(
|
| 17 |
description="Actual thematic content (not quality descriptors)"
|
| 18 |
)
|
| 19 |
-
date_range: list[int] = Field(description="Date range [min_year, max_year] (note: max_year is 2025)")
|
| 20 |
negative_keywords: list[str] = Field(description="List of negative keywords")
|
| 21 |
production_region: list[str] = Field(description="Production region")
|
| 22 |
min_rating: Optional[float] = Field(
|
|
|
|
| 16 |
themes: list[str] = Field(
|
| 17 |
description="Actual thematic content (not quality descriptors)"
|
| 18 |
)
|
| 19 |
+
date_range: list[int] = Field(description="Date range [min_year, max_year] (note: min_year is 1900, max_year is 2025)")
|
| 20 |
negative_keywords: list[str] = Field(description="List of negative keywords")
|
| 21 |
production_region: list[str] = Field(description="Production region")
|
| 22 |
min_rating: Optional[float] = Field(
|
models/recommendation_engine.py
CHANGED
|
@@ -8,6 +8,7 @@ from components.filters import MovieFilter
|
|
| 8 |
from sentence_transformers import SentenceTransformer
|
| 9 |
from components.tmdb_api import TMDBApi
|
| 10 |
|
|
|
|
| 11 |
class RecommendationEngine:
|
| 12 |
def __init__(self):
|
| 13 |
self.config = Config()
|
|
@@ -36,16 +37,6 @@ class RecommendationEngine:
|
|
| 36 |
search_results = self.similarity_calc.calculate_similarity(
|
| 37 |
features.themes, filtered_data, top_k
|
| 38 |
)
|
| 39 |
-
if search_results["results"]:
|
| 40 |
-
print(f"🔍 First result keys: {search_results['results'][0].keys()}")
|
| 41 |
-
|
| 42 |
-
for i, result in enumerate(search_results["results"]):
|
| 43 |
-
print(f"🔍 Result {i}: tconst = {result.get('tconst', 'NOT FOUND')}")
|
| 44 |
-
|
| 45 |
-
search_results["results"] = self.tmdb_api.get_multiple_posters_by_imdb(
|
| 46 |
-
search_results["results"]
|
| 47 |
-
)
|
| 48 |
-
|
| 49 |
formatted_results = self._format_results(search_results)
|
| 50 |
|
| 51 |
return formatted_results, self._create_results_dataframe(search_results)
|
|
@@ -66,17 +57,13 @@ class RecommendationEngine:
|
|
| 66 |
],
|
| 67 |
response_format=Features,
|
| 68 |
)
|
| 69 |
-
|
| 70 |
-
|
| 71 |
response_model = response.choices[0].message.parsed
|
| 72 |
|
| 73 |
print(type(response_model))
|
| 74 |
print(response_model.model_dump_json(indent=2))
|
| 75 |
-
print(f"✅ Parsed features: {response_model}")
|
| 76 |
return response_model
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
except Exception as e:
|
| 81 |
print(f"❌ Error parsing user query: {str(e)}")
|
| 82 |
return Features(
|
|
@@ -125,6 +112,7 @@ class RecommendationEngine:
|
|
| 125 |
for result in search_results["results"]:
|
| 126 |
df_data.append(
|
| 127 |
{
|
|
|
|
| 128 |
"Title": result["title"],
|
| 129 |
"Type": result["type"],
|
| 130 |
"Year": result["year"],
|
|
|
|
| 8 |
from sentence_transformers import SentenceTransformer
|
| 9 |
from components.tmdb_api import TMDBApi
|
| 10 |
|
| 11 |
+
|
| 12 |
class RecommendationEngine:
|
| 13 |
def __init__(self):
|
| 14 |
self.config = Config()
|
|
|
|
| 37 |
search_results = self.similarity_calc.calculate_similarity(
|
| 38 |
features.themes, filtered_data, top_k
|
| 39 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 40 |
formatted_results = self._format_results(search_results)
|
| 41 |
|
| 42 |
return formatted_results, self._create_results_dataframe(search_results)
|
|
|
|
| 57 |
],
|
| 58 |
response_format=Features,
|
| 59 |
)
|
| 60 |
+
|
|
|
|
| 61 |
response_model = response.choices[0].message.parsed
|
| 62 |
|
| 63 |
print(type(response_model))
|
| 64 |
print(response_model.model_dump_json(indent=2))
|
|
|
|
| 65 |
return response_model
|
| 66 |
+
|
|
|
|
|
|
|
| 67 |
except Exception as e:
|
| 68 |
print(f"❌ Error parsing user query: {str(e)}")
|
| 69 |
return Features(
|
|
|
|
| 112 |
for result in search_results["results"]:
|
| 113 |
df_data.append(
|
| 114 |
{
|
| 115 |
+
"ImdbId": result["tconst"],
|
| 116 |
"Title": result["title"],
|
| 117 |
"Type": result["type"],
|
| 118 |
"Year": result["year"],
|
tmdb_api.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import requests
|
| 2 |
+
from config import Config
|
| 3 |
+
|
| 4 |
+
class TMDBApi:
|
| 5 |
+
def __init__(self):
|
| 6 |
+
self.config = Config()
|
| 7 |
+
self.base_url = self.config.TMDB_BASE_URL
|
| 8 |
+
self.api_key = self.config.TMDB_API_KEY
|
| 9 |
+
self.image_base_url = self.config.TMDB_IMAGE_BASE_URL
|
| 10 |
+
|
| 11 |
+
def get_poster_by_imdb_id(self, imdb_id: str):
|
| 12 |
+
try:
|
| 13 |
+
if not imdb_id.startswith('tt'):
|
| 14 |
+
imdb_id = f"tt{imdb_id}"
|
| 15 |
+
|
| 16 |
+
endpoint = f"{self.base_url}/find/{imdb_id}"
|
| 17 |
+
params = {
|
| 18 |
+
"api_key": self.api_key,
|
| 19 |
+
"external_source": "tconst"
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
response = requests.get(endpoint, params=params)
|
| 23 |
+
response.raise_for_status()
|
| 24 |
+
|
| 25 |
+
data = response.json()
|
| 26 |
+
|
| 27 |
+
poster_path = None
|
| 28 |
+
if data.get("movie_results"):
|
| 29 |
+
poster_path = data["movie_results"][0].get("poster_path")
|
| 30 |
+
elif data.get("tv_results"):
|
| 31 |
+
poster_path = data["tv_results"][0].get("poster_path")
|
| 32 |
+
|
| 33 |
+
if poster_path:
|
| 34 |
+
return f"{self.image_base_url}{poster_path}"
|
| 35 |
+
return None
|
| 36 |
+
|
| 37 |
+
except Exception as e:
|
| 38 |
+
print(f"❌ TMDB API Error for IMDB ID {imdb_id}: {str(e)}")
|
| 39 |
+
return None
|
| 40 |
+
|
| 41 |
+
def get_multiple_posters_by_imdb(self, items: list):
|
| 42 |
+
results = []
|
| 43 |
+
for item in items:
|
| 44 |
+
imdb_id = item.get('tconst')
|
| 45 |
+
|
| 46 |
+
if imdb_id:
|
| 47 |
+
poster_url = self.get_poster_by_imdb_id(imdb_id)
|
| 48 |
+
item['poster_url'] = poster_url
|
| 49 |
+
else:
|
| 50 |
+
item['poster_url'] = None
|
| 51 |
+
|
| 52 |
+
results.append(item)
|
| 53 |
+
|
| 54 |
+
return results
|