Files
sublogue/server/core/tmdb_client.py
T
ponzischeme89 3ad3d9bfe0 Initial commit
2026-01-17 21:49:22 +13:00

322 lines
12 KiB
Python

"""
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