Files

175 lines
5.8 KiB
Python

import json
import logging
from typing import Optional, Dict, List
import httpx
from db import schemas
from db.config import settings
from db.redis_database import REDIS_ASYNC_CLIENT
class MDBListScraper:
def __init__(self, api_key: str):
self.api_key = api_key
self.base_url = "https://api.mdblist.com"
self.client = httpx.AsyncClient(proxy=settings.requests_proxy_url, timeout=30.0)
self.batch_size = 200
async def close(self):
await self.client.aclose()
async def _fetch_list(
self,
list_config: schemas.MDBListItem,
offset: int = 0,
genre: Optional[str] = None,
) -> Optional[Dict]:
"""Fetch a list from MDBList API"""
params = {
"apikey": self.api_key,
"limit": self.batch_size,
"offset": offset,
"append_to_response": "genre",
"sort": list_config.sort,
"order": list_config.order,
}
if genre:
params["filter_genre"] = genre
# Update cache key to include sort and order
cache_key = f"mdblist:raw:{list_config.id}:offset_{offset}:{genre or 'all'}:sort_{list_config.sort}:order_{list_config.order}"
cached_data = await REDIS_ASYNC_CLIENT.get(cache_key)
if cached_data:
return json.loads(cached_data)
try:
response = await self.client.get(
f"{self.base_url}/lists/{list_config.id}/items", params=params
)
if response.status_code == 200:
# Cache raw API response for 60 minutes
await REDIS_ASYNC_CLIENT.set(cache_key, response.text, ex=3600)
return response.json()
logging.error(f"Failed to fetch MDBList data: {response.status_code}")
return None
except Exception as e:
logging.error(f"Error fetching MDBList data: {e}")
return None
@staticmethod
def _convert_to_meta(item: Dict, media_type: str) -> schemas.Meta:
"""Convert MDBList item to Meta object"""
genres = item.get("genre", [])
if genres and genres[0] is None:
genres = None
return schemas.Meta.model_validate(
{
"_id": item["imdb_id"],
"type": media_type,
"title": item["title"],
"year": item["release_year"],
"genres": genres,
"poster": f"{settings.poster_host_url}/poster/{media_type}/{item['imdb_id']}.jpg",
}
)
async def get_all_list_items(
self,
list_config: schemas.MDBListItem,
genre: Optional[str] = None,
) -> List[str]:
"""
Fetch all IMDb IDs from a list until no more results are available.
Used for filtered results to ensure complete dataset.
"""
cache_key = f"mdblist:all_ids:{list_config.id}:{list_config.catalog_type}:{genre or 'all'}:sort_{list_config.sort}:order_{list_config.order}"
cached_ids = await REDIS_ASYNC_CLIENT.lrange(cache_key, 0, -1)
if cached_ids:
return [cached_id.decode() for cached_id in cached_ids]
all_imdb_ids = []
offset = 0
while True:
batch = await self._fetch_list(list_config, offset, genre)
if not batch:
break
# Get the correct list based on media type
items = batch.get(
"movies" if list_config.catalog_type == "movie" else "shows", []
)
if not items:
break
# Filter valid IMDb IDs for the specific media type
new_ids = [
item["imdb_id"]
for item in items
if item.get("imdb_id", "").startswith("tt")
]
if not new_ids:
break
all_imdb_ids.extend(new_ids)
offset += self.batch_size
# If we got fewer items than batch_size, we've reached the end
if len(items) < self.batch_size:
break
# Cache the complete list if we got any results
if all_imdb_ids:
pipeline = await REDIS_ASYNC_CLIENT.pipeline()
pipeline.delete(cache_key)
pipeline.rpush(cache_key, *all_imdb_ids)
pipeline.expire(cache_key, 3600) # Cache for 1 hour
await pipeline.execute()
return all_imdb_ids
async def get_list_items(
self,
list_config: schemas.MDBListItem,
skip: int = 0,
limit: int = 25,
genre: Optional[str] = None,
use_filters: bool = True,
) -> List[str] | List[schemas.Meta]:
"""
Get items from a MDBList list.
For filtered results (use_filters=True), fetches all available items.
For unfiltered results, uses regular pagination.
"""
if use_filters:
return await self.get_all_list_items(list_config, genre)
# For unfiltered results, use regular pagination
offset = (skip // self.batch_size) * self.batch_size
batch = await self._fetch_list(list_config, offset, genre)
if not batch:
return []
items = batch.get(
"movies" if list_config.catalog_type == "movie" else "shows", []
)
meta_list = [
self._convert_to_meta(item, list_config.catalog_type)
for item in items
if item.get("imdb_id", "").startswith("tt")
]
# Calculate the slice we need from this batch
start_idx = skip % self.batch_size
end_idx = min(start_idx + limit, len(meta_list))
return meta_list[start_idx:end_idx]
async def initialize_mdblist_scraper(api_key: str) -> MDBListScraper:
"""Initialize MDBList scraper with API key"""
return MDBListScraper(api_key)