import ssl, requests, time from typing import Dict, Any, Optional, List from requests.adapters import HTTPAdapter from urllib3.util.retry import Retry from fastapi import HTTPException from core.config import TMDB_API_KEY, TMDB_BASE, TMDB_IMG_500 from core.models import TMDBMovieCard, TMDBMovieDetails _TMDB_RETRY = Retry(total=5, backoff_factor=0.5, status_forcelist=[500, 502, 503, 504], allowed_methods=["GET"]) _TMDB_SESSION = requests.Session() _TMDB_SESSION.mount("https://", HTTPAdapter(max_retries=_TMDB_RETRY)) def _norm_title(t: str) -> str: return str(t).strip().lower() def make_img_url(path: Optional[str]) -> Optional[str]: if not path: return None return f"{TMDB_IMG_500}{path}" async def tmdb_get(path: str, params: Dict[str, Any]) -> Dict[str, Any]: q = dict(params) q["api_key"] = TMDB_API_KEY last_err = None for attempt in range(1, 4): print(f"DEBUG: TMDB Fetch Attempt {attempt} | Path: {path}") try: r = _TMDB_SESSION.get(f"{TMDB_BASE}{path}", params=q, timeout=12) break except requests.exceptions.Timeout: last_err = "Request Timed Out (12s)" print(f"DEBUG: TMDB Timeout on attempt {attempt}") continue except Exception as e: last_err = str(e) print(f"DEBUG: TMDB Error on attempt {attempt}: {e}") time.sleep(0.3 * attempt) else: raise HTTPException( status_code=504, detail=f"TMDB Gateway Timeout after 3 attempts. Last error: {last_err}", ) if r.status_code != 200: print(f"DEBUG: TMDB non-200. Code: {r.status_code} | Body: {r.text[:100]}") raise HTTPException( status_code=502, detail=f"TMDB API Remote Error {r.status_code}" ) return r.json() async def tmdb_cards_from_results( results: List[dict], limit: int = 20 ) -> List[TMDBMovieCard]: out: List[TMDBMovieCard] = [] for m in (results or [])[:limit]: out.append( TMDBMovieCard( tmdb_id=int(m["id"]), title=m.get("title") or m.get("name") or "", poster_url=make_img_url(m.get("poster_path")), release_date=m.get("release_date"), vote_average=m.get("vote_average"), ) ) return out async def tmdb_search_first(query: str) -> Optional[dict]: data = await tmdb_get( "/search/movie", { "query": query, "include_adult": "false", "language": "en-US", "page": 1, }, ) results = data.get("results", []) return results[0] if results else None async def tmdb_search_movies(query: str, page: int = 1) -> Dict[str, Any]: return await tmdb_get( "/search/movie", { "query": query, "include_adult": "false", "language": "en-US", "page": page, }, ) async def tmdb_movie_details(movie_id: int) -> TMDBMovieDetails: data = await tmdb_get(f"/movie/{movie_id}", {"language": "en-US", "append_to_response": "videos,watch/providers,reviews"}) trailer_key = None videos = data.get("videos", {}).get("results", []) for v in videos: if v.get("site") == "YouTube" and v.get("type") == "Trailer": trailer_key = v.get("key") break providers_data = [] us_providers = data.get("watch/providers", {}).get("results", {}).get("US", {}) watch_link = us_providers.get("link", "") for p_type in ["flatrate", "rent", "buy"]: if p_type in us_providers: for p in us_providers[p_type]: providers_data.append({ "name": p.get("provider_name"), "logo_url": make_img_url(p.get("logo_path")) }) break col_data = None raw_col = data.get("belongs_to_collection") if raw_col: col_data = { "id": raw_col.get("id"), "name": raw_col.get("name"), "poster_url": make_img_url(raw_col.get("poster_path")), } rev_data = [] raw_revs = data.get("reviews", {}).get("results", [])[:3] for r in raw_revs: rev_data.append({ "author": r.get("author", "Unknown"), "content": r.get("content", ""), "url": r.get("url", "") }) return TMDBMovieDetails( tmdb_id=int(data["id"]), title=data.get("title") or "", overview=data.get("overview"), release_date=data.get("release_date"), poster_url=make_img_url(data.get("poster_path")), backdrop_url=make_img_url(data.get("backdrop_path")), trailer_youtube_id=trailer_key, watch_providers=providers_data, watch_link=watch_link, collection=col_data, reviews=rev_data, genres=data.get("genres", []) or [], )