Files
sublogue/server/core/tvmaze_client.py
T

137 lines
4.7 KiB
Python
Raw Normal View History

2026-01-17 21:49:22 +13:00
"""
TVmaze API client - async TV metadata fetching
"""
import aiohttp
import logging
2026-01-18 23:01:03 +13:00
from logging_utils import get_logger
2026-01-17 21:49:22 +13:00
import re
import time
2026-01-18 23:01:03 +13:00
logger = get_logger(__name__)
2026-01-17 21:49:22 +13:00
class TVMazeClient:
"""Async client for the TVmaze API (no API key required)."""
BASE_URL = "https://api.tvmaze.com"
def __init__(self, db_manager=None, timeout=15):
self.db_manager = db_manager
self._timeout = aiohttp.ClientTimeout(total=timeout)
async def fetch_summary(self, title, year=None, season=None, episode=None):
"""
Fetch summary for a TV series (optionally episode-specific).
Args:
title: Series title to search
year: Optional year to validate match
season: Optional season number
episode: Optional episode number
"""
show = await self._fetch_show(title, year)
if not show:
return None
plot = self._strip_html(show.get("summary")) or "No plot available"
if season is not None and episode is not None:
episode_data = await self._fetch_episode(show.get("id"), season, episode)
if episode_data:
episode_summary = self._strip_html(episode_data.get("summary"))
if episode_summary:
plot = episode_summary
rating = show.get("rating", {}).get("average")
imdb_rating = f"{rating:.1f}" if isinstance(rating, (int, float)) else "N/A"
runtime_value = show.get("runtime") or show.get("averageRuntime")
runtime = f"{runtime_value} min" if runtime_value else "N/A"
premiered = show.get("premiered") or ""
year_value = premiered[:4] if premiered else "N/A"
return {
"plot": plot,
"title": show.get("name", title),
"year": year_value,
"media_type": "tv",
"imdb_rating": imdb_rating,
"rotten_tomatoes": "N/A",
"runtime": runtime,
}
async def _fetch_show(self, title, year):
params = {"q": title}
url = f"{self.BASE_URL}/singlesearch/shows"
start_time = time.time()
try:
async with aiohttp.ClientSession(timeout=self._timeout) as session:
async with session.get(url, params=params) as response:
response_time_ms = int((time.time() - start_time) * 1000)
if response.status != 200:
self._track(False, "/singlesearch/shows", response_time_ms)
logger.error("TVmaze HTTP %s for '%s'", response.status, title)
return None
data = await response.json()
self._track(True, "/singlesearch/shows", response_time_ms)
if year:
premiered = data.get("premiered") or ""
if premiered and premiered[:4] != year:
logger.warning(
"TVmaze year mismatch for '%s': requested %s, got %s",
title,
year,
premiered[:4],
)
return None
return data
except Exception as e:
logger.error("TVmaze error for '%s': %s", title, e)
return None
async def _fetch_episode(self, show_id, season, episode):
if not show_id:
return None
params = {"season": season, "number": episode}
url = f"{self.BASE_URL}/shows/{show_id}/episodebynumber"
start_time = time.time()
try:
async with aiohttp.ClientSession(timeout=self._timeout) as session:
async with session.get(url, params=params) as response:
response_time_ms = int((time.time() - start_time) * 1000)
if response.status != 200:
self._track(False, "/shows/{id}/episodebynumber", response_time_ms)
return None
data = await response.json()
self._track(True, "/shows/{id}/episodebynumber", response_time_ms)
return data
except Exception as e:
logger.error("TVmaze episode error for show %s: %s", show_id, e)
return None
def _track(self, success, endpoint, response_time_ms):
if not self.db_manager:
return
self.db_manager.track_api_call(
provider="tvmaze",
endpoint=endpoint,
success=success,
response_time_ms=response_time_ms,
)
@staticmethod
def _strip_html(text):
if not text:
return ""
return re.sub(r"<[^>]+>", "", text).strip()