Initial commit
This commit is contained in:
@@ -0,0 +1,321 @@
|
||||
"""
|
||||
TMDb API client - async movie and TV series metadata fetching
|
||||
"""
|
||||
import asyncio
|
||||
import aiohttp
|
||||
import logging
|
||||
import time
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
|
||||
class TMDbClient:
|
||||
"""Async client for The Movie Database (TMDb) API"""
|
||||
|
||||
def __init__(self, api_key, db_manager=None):
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.themoviedb.org/3"
|
||||
self.semaphore = asyncio.Semaphore(5) # Limit concurrent requests
|
||||
self.db_manager = db_manager
|
||||
|
||||
async def search_movie(self, title, year=None):
|
||||
"""
|
||||
Search for a movie by title
|
||||
|
||||
Args:
|
||||
title: Movie title to search
|
||||
year: Optional year to narrow search
|
||||
|
||||
Returns:
|
||||
dict: Movie data or None if not found
|
||||
"""
|
||||
async with self.semaphore:
|
||||
url = f"{self.base_url}/search/movie"
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"query": title
|
||||
}
|
||||
if year:
|
||||
params["year"] = year
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
response_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
if response.status != 200:
|
||||
logging.error(f"TMDb HTTP error {response.status} for movie '{title}'")
|
||||
# Track failed API call
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/movie',
|
||||
success=False,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return None
|
||||
|
||||
data = await response.json()
|
||||
|
||||
if data.get("results") and len(data["results"]) > 0:
|
||||
# Track successful API call
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/movie',
|
||||
success=True,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return data["results"][0] # Return first match
|
||||
|
||||
logging.warning(f"No TMDb results for movie '{title}'")
|
||||
# Track failed API call (no results)
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/movie',
|
||||
success=False,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error searching TMDb for movie '{title}': {e}")
|
||||
return None
|
||||
|
||||
async def search_tv(self, title, year=None):
|
||||
"""
|
||||
Search for a TV series by title
|
||||
|
||||
Args:
|
||||
title: TV series title to search
|
||||
year: Optional year to narrow search
|
||||
|
||||
Returns:
|
||||
dict: TV series data or None if not found
|
||||
"""
|
||||
async with self.semaphore:
|
||||
url = f"{self.base_url}/search/tv"
|
||||
params = {
|
||||
"api_key": self.api_key,
|
||||
"query": title
|
||||
}
|
||||
if year:
|
||||
params["first_air_date_year"] = year
|
||||
|
||||
try:
|
||||
start_time = time.time()
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
response_time_ms = int((time.time() - start_time) * 1000)
|
||||
|
||||
if response.status != 200:
|
||||
logging.error(f"TMDb HTTP error {response.status} for TV '{title}'")
|
||||
# Track failed API call
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/tv',
|
||||
success=False,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return None
|
||||
|
||||
data = await response.json()
|
||||
|
||||
if data.get("results") and len(data["results"]) > 0:
|
||||
# Track successful API call
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/tv',
|
||||
success=True,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return data["results"][0] # Return first match
|
||||
|
||||
logging.warning(f"No TMDb results for TV series '{title}'")
|
||||
# Track failed API call (no results)
|
||||
if self.db_manager:
|
||||
self.db_manager.track_api_call(
|
||||
provider='tmdb',
|
||||
endpoint='/search/tv',
|
||||
success=False,
|
||||
response_time_ms=response_time_ms
|
||||
)
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error searching TMDb for TV '{title}': {e}")
|
||||
return None
|
||||
|
||||
async def get_movie_details(self, movie_id):
|
||||
"""
|
||||
Get detailed movie information
|
||||
|
||||
Args:
|
||||
movie_id: TMDb movie ID
|
||||
|
||||
Returns:
|
||||
dict: Detailed movie data
|
||||
"""
|
||||
async with self.semaphore:
|
||||
url = f"{self.base_url}/movie/{movie_id}"
|
||||
params = {"api_key": self.api_key}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status != 200:
|
||||
logging.error(f"TMDb HTTP error {response.status} for movie ID {movie_id}")
|
||||
return None
|
||||
|
||||
return await response.json()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting TMDb movie details for ID {movie_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_tv_details(self, tv_id):
|
||||
"""
|
||||
Get detailed TV series information
|
||||
|
||||
Args:
|
||||
tv_id: TMDb TV series ID
|
||||
|
||||
Returns:
|
||||
dict: Detailed TV series data
|
||||
"""
|
||||
async with self.semaphore:
|
||||
url = f"{self.base_url}/tv/{tv_id}"
|
||||
params = {"api_key": self.api_key}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status != 200:
|
||||
logging.error(f"TMDb HTTP error {response.status} for TV ID {tv_id}")
|
||||
return None
|
||||
|
||||
return await response.json()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting TMDb TV details for ID {tv_id}: {e}")
|
||||
return None
|
||||
|
||||
async def get_tv_season(self, tv_id, season_number):
|
||||
"""
|
||||
Get TV season information
|
||||
|
||||
Args:
|
||||
tv_id: TMDb TV series ID
|
||||
season_number: Season number
|
||||
|
||||
Returns:
|
||||
dict: Season data including episodes
|
||||
"""
|
||||
async with self.semaphore:
|
||||
url = f"{self.base_url}/tv/{tv_id}/season/{season_number}"
|
||||
params = {"api_key": self.api_key}
|
||||
|
||||
try:
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params) as response:
|
||||
if response.status != 200:
|
||||
logging.error(f"TMDb HTTP error {response.status} for TV {tv_id} season {season_number}")
|
||||
return None
|
||||
|
||||
return await response.json()
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting TMDb season data: {e}")
|
||||
return None
|
||||
|
||||
async def fetch_summary(self, title, media_type="movie", year=None, season=None, episode=None):
|
||||
"""
|
||||
Fetch summary for movie or TV series
|
||||
|
||||
Args:
|
||||
title: Title to search
|
||||
media_type: "movie" or "tv"
|
||||
year: Optional year
|
||||
season: Optional season number (for TV)
|
||||
episode: Optional episode number (for TV)
|
||||
|
||||
Returns:
|
||||
dict: {plot, title, year, media_type, rating} or None if not found
|
||||
"""
|
||||
logging.info(f"Fetching TMDb summary for: {title} (type: {media_type})")
|
||||
|
||||
try:
|
||||
if media_type == "tv":
|
||||
# Search for TV series
|
||||
search_result = await self.search_tv(title, year)
|
||||
if not search_result:
|
||||
return None
|
||||
|
||||
tv_id = search_result["id"]
|
||||
|
||||
# Get detailed TV info
|
||||
tv_details = await self.get_tv_details(tv_id)
|
||||
if not tv_details:
|
||||
return None
|
||||
|
||||
plot = tv_details.get("overview", "No plot available")
|
||||
|
||||
# If specific season/episode requested, try to get that plot
|
||||
if season is not None:
|
||||
season_data = await self.get_tv_season(tv_id, season)
|
||||
if season_data and episode is not None:
|
||||
episodes = season_data.get("episodes", [])
|
||||
for ep in episodes:
|
||||
if ep.get("episode_number") == episode:
|
||||
plot = ep.get("overview", plot)
|
||||
break
|
||||
|
||||
# Get episode runtime (usually consistent across series)
|
||||
runtime = "N/A"
|
||||
episode_run_time = tv_details.get("episode_run_time", [])
|
||||
if episode_run_time and len(episode_run_time) > 0:
|
||||
runtime = f"{episode_run_time[0]} min"
|
||||
|
||||
return {
|
||||
"plot": plot,
|
||||
"title": tv_details.get("name", title),
|
||||
"year": tv_details.get("first_air_date", "")[:4] if tv_details.get("first_air_date") else "N/A",
|
||||
"media_type": "tv",
|
||||
"rating": f"{tv_details.get('vote_average', 0):.1f}/10",
|
||||
"runtime": runtime
|
||||
}
|
||||
|
||||
else: # movie
|
||||
# Search for movie
|
||||
search_result = await self.search_movie(title, year)
|
||||
if not search_result:
|
||||
return None
|
||||
|
||||
movie_id = search_result["id"]
|
||||
|
||||
# Get detailed movie info
|
||||
movie_details = await self.get_movie_details(movie_id)
|
||||
if not movie_details:
|
||||
return None
|
||||
|
||||
# Get runtime
|
||||
runtime = "N/A"
|
||||
if movie_details.get("runtime"):
|
||||
runtime = f"{movie_details.get('runtime')} min"
|
||||
|
||||
return {
|
||||
"plot": movie_details.get("overview", "No plot available"),
|
||||
"title": movie_details.get("title", title),
|
||||
"year": movie_details.get("release_date", "")[:4] if movie_details.get("release_date") else "N/A",
|
||||
"media_type": "movie",
|
||||
"rating": f"{movie_details.get('vote_average', 0):.1f}/10",
|
||||
"runtime": runtime
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Error fetching TMDb summary for '{title}': {e}")
|
||||
return None
|
||||
Reference in New Issue
Block a user