675 lines
24 KiB
Python
675 lines
24 KiB
Python
"""
|
|
Catalog Router
|
|
Movie catalog and streaming endpoints for ophim/PhimMoiChill integration
|
|
"""
|
|
import asyncio
|
|
import aiohttp
|
|
import ssl
|
|
import re
|
|
from typing import Optional, Dict, List
|
|
from fastapi import APIRouter, HTTPException, Depends, Query
|
|
from fastapi.responses import JSONResponse
|
|
from pydantic import BaseModel
|
|
|
|
from cache import cache
|
|
from config import settings
|
|
from security import verify_hmac
|
|
from logging_config import get_logger
|
|
|
|
logger = get_logger("catalog")
|
|
|
|
router = APIRouter(prefix="/api/rophim", tags=["catalog"])
|
|
|
|
|
|
# Category slug mappings
|
|
CATEGORY_MAP = {
|
|
# Main categories
|
|
'movies': 'danh-sach/phim-le',
|
|
'series': 'danh-sach/phim-bo',
|
|
'tv-shows': 'danh-sach/phim-bo',
|
|
'animation': 'danh-sach/hoat-hinh',
|
|
'cinema': 'danh-sach/phim-chieu-rap',
|
|
# Vietnamese slugs (passthrough)
|
|
'phim-le': 'danh-sach/phim-le',
|
|
'phim-bo': 'danh-sach/phim-bo',
|
|
'phim-moi': 'danh-sach/phim-moi-cap-nhat',
|
|
'phim-moi-cap-nhat': 'danh-sach/phim-moi-cap-nhat',
|
|
'hoat-hinh': 'danh-sach/hoat-hinh',
|
|
'phim-chieu-rap': 'danh-sach/phim-chieu-rap',
|
|
# New/trending/popular
|
|
'trending': 'danh-sach/phim-moi-cap-nhat',
|
|
'new': 'danh-sach/phim-le',
|
|
'popular': 'danh-sach/phim-le',
|
|
'all': 'danh-sach/phim-le',
|
|
# Genre categories
|
|
'action': 'the-loai/hanh-dong',
|
|
'comedy': 'the-loai/hai-huoc',
|
|
'drama': 'the-loai/chinh-kich',
|
|
'horror': 'the-loai/kinh-di',
|
|
'romance': 'the-loai/tinh-cam',
|
|
'scifi': 'the-loai/vien-tuong',
|
|
# Country categories
|
|
'korean': 'quoc-gia/han-quoc',
|
|
'han-quoc': 'quoc-gia/han-quoc',
|
|
'usa': 'quoc-gia/au-my',
|
|
'au-my': 'quoc-gia/au-my',
|
|
'china': 'quoc-gia/trung-quoc',
|
|
'trung-quoc': 'quoc-gia/trung-quoc',
|
|
'japan': 'quoc-gia/nhat-ban',
|
|
'nhat-ban': 'quoc-gia/nhat-ban',
|
|
'thailand': 'quoc-gia/thai-lan',
|
|
'thai-lan': 'quoc-gia/thai-lan',
|
|
'vietnam': 'quoc-gia/viet-nam',
|
|
'viet-nam': 'quoc-gia/viet-nam',
|
|
'my': 'quoc-gia/au-my',
|
|
'hong-kong': 'quoc-gia/hong-kong',
|
|
'dai-loan': 'quoc-gia/dai-loan',
|
|
'an-do': 'quoc-gia/an-do',
|
|
# Additional genre mappings
|
|
'hanh-dong': 'the-loai/hanh-dong',
|
|
'kinh-di': 'the-loai/kinh-di',
|
|
'tinh-cam': 'the-loai/tinh-cam',
|
|
'vien-tuong': 'the-loai/vien-tuong',
|
|
'hai-huoc': 'the-loai/hai-huoc',
|
|
'han-quoc-hits': 'quoc-gia/han-quoc',
|
|
'phieu-luu': 'the-loai/phieu-luu',
|
|
'vo-thuat': 'the-loai/vo-thuat',
|
|
'hinh-su': 'the-loai/hinh-su',
|
|
'tai-lieu': 'the-loai/tai-lieu',
|
|
'gia-dinh': 'the-loai/gia-dinh',
|
|
'co-trang': 'the-loai/co-trang',
|
|
'hoc-duong': 'the-loai/hoc-duong',
|
|
'tam-ly': 'the-loai/tam-ly',
|
|
'than-thoai': 'the-loai/than-thoai',
|
|
'chien-tranh': 'the-loai/chien-tranh',
|
|
'the-thao': 'the-loai/the-thao',
|
|
'am-nhac': 'the-loai/am-nhac',
|
|
}
|
|
|
|
|
|
def _get_ssl_connector():
|
|
"""Create SSL connector with verification disabled for ophim"""
|
|
ssl_ctx = ssl.create_default_context()
|
|
ssl_ctx.check_hostname = False
|
|
ssl_ctx.verify_mode = ssl.CERT_NONE
|
|
return aiohttp.TCPConnector(ssl=ssl_ctx)
|
|
|
|
|
|
def _parse_movie_item(item: Dict) -> Dict:
|
|
"""Parse a movie item from ophim API response"""
|
|
tmdb_data = item.get('tmdb', {})
|
|
imdb_data = item.get('imdb', {})
|
|
|
|
tmdb_rating = tmdb_data.get('vote_average', 0) or 0
|
|
imdb_rating = imdb_data.get('vote_average', 0) or 0
|
|
best_rating = max(tmdb_rating, imdb_rating)
|
|
|
|
return {
|
|
'id': item.get('slug', ''),
|
|
'title': item.get('name', ''),
|
|
'original_title': item.get('origin_name'),
|
|
'slug': item.get('slug', ''),
|
|
'thumbnail': f"https://img.ophim.live/uploads/movies/{item.get('thumb_url', '')}",
|
|
'poster_url': f"https://img.ophim.live/uploads/movies/{item.get('poster_url', '')}",
|
|
'year': item.get('year'),
|
|
'quality': item.get('quality', 'HD'),
|
|
'duration': item.get('time'),
|
|
'category': item.get('type', 'single'),
|
|
'tmdb_rating': tmdb_rating,
|
|
'imdb_rating': imdb_rating,
|
|
'rating': best_rating,
|
|
'vote_count': tmdb_data.get('vote_count', 0),
|
|
'genres': [cat.get('name') for cat in item.get('category', [])],
|
|
'country': [c.get('name') for c in item.get('country', [])],
|
|
'modified': item.get('modified', {}).get('time'),
|
|
'episode_current': item.get('episode_current'),
|
|
'lang': item.get('lang'),
|
|
}
|
|
|
|
|
|
def _resolve_category_slug(category: Optional[str]) -> str:
|
|
"""Resolve category to ophim API slug"""
|
|
if not category:
|
|
return 'danh-sach/phim-le'
|
|
|
|
# Use mapped slug or fallback to input as-is
|
|
slug = CATEGORY_MAP.get(category, f'danh-sach/{category}')
|
|
|
|
# If category starts with known prefixes, use as-is
|
|
if category.startswith(('danh-sach/', 'the-loai/', 'quoc-gia/')):
|
|
slug = category
|
|
|
|
return slug
|
|
|
|
|
|
@router.get("/catalog")
|
|
async def get_catalog(
|
|
category: Optional[str] = None,
|
|
page: int = Query(1, ge=1),
|
|
limit: int = Query(24, ge=1, le=50),
|
|
sort: str = Query("modified", description="Sort by: modified, year, rating"),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get movie catalog from ophim API with sorting support."""
|
|
# Check cache first
|
|
cache_key = f"catalog:{category}:{page}:{limit}:{sort}"
|
|
cached = cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
slug = _resolve_category_slug(category)
|
|
|
|
try:
|
|
connector = _get_ssl_connector()
|
|
|
|
async with aiohttp.ClientSession(connector=connector) as session:
|
|
api_url = f"https://ophim1.com/v1/api/{slug}?page={page}"
|
|
|
|
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=settings.request_timeout)) as resp:
|
|
if resp.status != 200:
|
|
logger.warning(f"Slug {slug} failed ({resp.status}), falling back to phim-le")
|
|
api_url = f"https://ophim1.com/v1/api/danh-sach/phim-le?page={page}"
|
|
async with session.get(api_url) as fallback_resp:
|
|
data = await fallback_resp.json()
|
|
else:
|
|
data = await resp.json()
|
|
|
|
items = data.get('data', {}).get('items', [])
|
|
movies = [_parse_movie_item(item) for item in items]
|
|
|
|
# Apply sorting
|
|
if sort == 'year':
|
|
movies.sort(key=lambda x: x.get('year') or 0, reverse=True)
|
|
elif sort == 'rating':
|
|
movies.sort(key=lambda x: x.get('rating') or 0, reverse=True)
|
|
|
|
result = {
|
|
"movies": movies[:limit],
|
|
"page": page,
|
|
"category": category or 'movies',
|
|
"sort": sort,
|
|
"total": len(movies)
|
|
}
|
|
|
|
# Cache for 1 hour
|
|
cache.set(cache_key, result, ttl=settings.cache_catalog_ttl)
|
|
return result
|
|
|
|
except aiohttp.ClientError as e:
|
|
logger.error(f"Failed to fetch catalog: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch catalog: {str(e)}")
|
|
|
|
|
|
@router.get("/search")
|
|
async def search_movies(
|
|
q: str = Query(..., min_length=1),
|
|
limit: int = Query(20, ge=1, le=50),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Search movies by title AND actors using ophim API"""
|
|
movies = []
|
|
seen_slugs = set()
|
|
|
|
connector = _get_ssl_connector()
|
|
|
|
def add_movie(item):
|
|
"""Helper to add movie avoiding duplicates"""
|
|
slug = item.get('slug', '')
|
|
if slug and slug not in seen_slugs:
|
|
seen_slugs.add(slug)
|
|
movies.append({
|
|
'id': slug,
|
|
'title': item.get('name', ''),
|
|
'original_title': item.get('origin_name'),
|
|
'slug': slug,
|
|
'thumbnail': f"https://img.ophim.live/uploads/movies/{item.get('thumb_url', '')}",
|
|
'backdrop': f"https://img.ophim.live/uploads/movies/{item.get('poster_url', '')}",
|
|
'year': item.get('year'),
|
|
'rating': None,
|
|
'duration': None,
|
|
'quality': item.get('quality', 'HD'),
|
|
'genre': None,
|
|
'description': None,
|
|
'category': item.get('type', 'movies')
|
|
})
|
|
|
|
async with aiohttp.ClientSession(connector=connector) as session:
|
|
# Search by movie title (primary)
|
|
try:
|
|
api_url = f"https://ophim1.com/v1/api/tim-kiem?keyword={q}&limit={limit}"
|
|
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
items = data.get('data', {}).get('items', [])
|
|
for item in items:
|
|
add_movie(item)
|
|
except Exception as e:
|
|
logger.warning(f"Title search failed: {e}")
|
|
|
|
# Search by actor name (secondary)
|
|
if len(movies) < limit:
|
|
try:
|
|
actor_slug = q.lower().replace(' ', '-')
|
|
actor_url = f"https://ophim1.com/v1/api/danh-sach/dien-vien/{actor_slug}"
|
|
async with session.get(actor_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
|
if resp.status == 200:
|
|
data = await resp.json()
|
|
items = data.get('data', {}).get('items', [])
|
|
for item in items:
|
|
if len(movies) >= limit:
|
|
break
|
|
add_movie(item)
|
|
except Exception as e:
|
|
logger.warning(f"Actor search failed: {e}")
|
|
|
|
# Fallback to phimmoichill scraper if no results
|
|
if not movies:
|
|
from rophim_scraper import RophimScraper
|
|
try:
|
|
scraper = RophimScraper()
|
|
try:
|
|
results = await scraper.search(q, limit)
|
|
movies = [movie.__dict__ for movie in results]
|
|
finally:
|
|
await scraper.close()
|
|
except Exception as e:
|
|
logger.warning(f"Scraper search failed: {e}")
|
|
|
|
return {"movies": movies[:limit], "total": len(movies)}
|
|
|
|
|
|
@router.get("/categories/discover")
|
|
async def discover_categories(authorized: bool = Depends(verify_hmac)):
|
|
"""Discover all available categories from PhimMoiChill"""
|
|
from category_discovery import get_categories
|
|
|
|
try:
|
|
categories = await get_categories()
|
|
totals = {cat_type: len(cat_list) for cat_type, cat_list in categories.items()}
|
|
|
|
return {
|
|
"categories": categories,
|
|
"totals": totals,
|
|
"total_categories": sum(totals.values())
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to discover categories: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to discover categories: {str(e)}")
|
|
|
|
|
|
@router.get("/home/curated")
|
|
async def get_curated_homepage_sections(authorized: bool = Depends(verify_hmac)):
|
|
"""Get curated homepage sections with TOP RATED, NEW RELEASES, and popular genres."""
|
|
cache_key = "home:curated_v2"
|
|
cached = cache.get(cache_key)
|
|
if cached:
|
|
return cached
|
|
|
|
connector = _get_ssl_connector()
|
|
|
|
async def fetch_section(session, title: str, slug: str, sort_key: str = None, limit: int = 15):
|
|
"""Fetch a single section"""
|
|
try:
|
|
api_url = f"https://ophim1.com/v1/api/{slug}?page=1"
|
|
async with session.get(api_url, timeout=aiohttp.ClientTimeout(total=10)) as resp:
|
|
if resp.status != 200:
|
|
return None
|
|
data = await resp.json()
|
|
items = data.get('data', {}).get('items', [])
|
|
|
|
movies = [_parse_movie_item(item) for item in items[:30]]
|
|
|
|
# Apply sorting
|
|
if sort_key == 'rating':
|
|
movies.sort(key=lambda x: (x.get('rating') or 0, x.get('vote_count') or 0), reverse=True)
|
|
elif sort_key == 'year':
|
|
movies.sort(key=lambda x: x.get('year') or 0, reverse=True)
|
|
|
|
return {'title': title, 'key': slug, 'movies': movies[:limit]}
|
|
except Exception as e:
|
|
logger.warning(f"Error fetching {title}: {e}")
|
|
return None
|
|
|
|
try:
|
|
async with aiohttp.ClientSession(connector=connector) as session:
|
|
section_configs = [
|
|
("🏆 Top Rated Movies", "danh-sach/phim-le", "rating"),
|
|
("🎬 New Releases", "danh-sach/phim-le", "year"),
|
|
("📺 Top Rated Series", "danh-sach/phim-bo", "rating"),
|
|
("💥 Action & Adventure", "the-loai/hanh-dong", "rating"),
|
|
("😱 Horror & Thriller", "the-loai/kinh-di", "rating"),
|
|
("❤️ Romance", "the-loai/tinh-cam", "rating"),
|
|
("🎭 Drama", "the-loai/chinh-kich", "rating"),
|
|
("😂 Comedy", "the-loai/hai-huoc", "rating"),
|
|
("🌟 Sci-Fi & Fantasy", "the-loai/vien-tuong", "rating"),
|
|
("🎌 Animation & Anime", "danh-sach/hoat-hinh", "rating"),
|
|
("🇰🇷 Korean Movies", "quoc-gia/han-quoc", "rating"),
|
|
("🇺🇸 Western Movies", "quoc-gia/au-my", "rating"),
|
|
]
|
|
|
|
tasks = [fetch_section(session, title, slug, sort_key) for title, slug, sort_key in section_configs]
|
|
results = await asyncio.gather(*tasks)
|
|
|
|
sections = [r for r in results if r and r.get('movies')]
|
|
|
|
result = {"sections": sections, "total": len(sections)}
|
|
cache.set(cache_key, result, ttl=settings.cache_home_ttl)
|
|
return result
|
|
|
|
except Exception as e:
|
|
logger.error(f"Error fetching curated sections: {e}")
|
|
return {"sections": [], "error": str(e)}
|
|
|
|
|
|
@router.get("/stream/{slug}")
|
|
async def get_stream(
|
|
slug: str,
|
|
episode: int = Query(1, ge=1),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get video stream URL from ophim API for a specific slug and episode."""
|
|
from rophim_scraper import get_video_stream
|
|
|
|
try:
|
|
logger.debug(f"Processing stream request for {slug} ep {episode}")
|
|
stream_url = await get_video_stream(slug, episode=episode)
|
|
|
|
if not stream_url:
|
|
logger.warning(f"Stream not found for {slug}")
|
|
return JSONResponse(status_code=404, content={"detail": "Stream not found"})
|
|
|
|
return {"stream_url": stream_url, "episode": episode, "slug": slug}
|
|
except Exception as e:
|
|
logger.error(f"Error in get_stream: {e}")
|
|
return JSONResponse(status_code=500, content={"detail": str(e)})
|
|
|
|
|
|
class StreamRequest(BaseModel):
|
|
"""Request for video stream URL"""
|
|
slug: str = ""
|
|
source_url: str = ""
|
|
episode: int = 1
|
|
server: int = 0
|
|
|
|
|
|
@router.post("/stream")
|
|
async def get_stream_post(
|
|
request: StreamRequest,
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get video stream URL (POST) - supports source_url if needed"""
|
|
import traceback
|
|
from rophim_scraper import get_video_stream
|
|
|
|
try:
|
|
slug = request.slug
|
|
if not slug and request.source_url:
|
|
# Extract slug from source_url
|
|
match = re.search(r'/phim/([^/\?]+)', request.source_url)
|
|
if match:
|
|
slug = match.group(1)
|
|
|
|
if not slug:
|
|
raise HTTPException(status_code=400, detail="Could not extract slug from URL")
|
|
|
|
stream_url = await get_video_stream(slug, episode=request.episode)
|
|
|
|
if not stream_url:
|
|
raise HTTPException(status_code=404, detail="Stream not found")
|
|
|
|
return JSONResponse(content={"stream_url": stream_url, "episode": request.episode, "slug": slug})
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Error in get_stream_post: {e}")
|
|
traceback.print_exc()
|
|
return JSONResponse(status_code=500, content={"detail": str(e)})
|
|
|
|
|
|
@router.get("/movie/{slug}")
|
|
async def get_movie_details(slug: str, authorized: bool = Depends(verify_hmac)):
|
|
"""Get detailed movie info from PhimMoiChill with optional TMDB enrichment"""
|
|
from rophim_scraper import get_movie_details as fetch_movie_details
|
|
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
movie = await loop.run_in_executor(None, lambda: fetch_movie_details(slug))
|
|
|
|
if not movie:
|
|
raise HTTPException(status_code=404, detail="Movie not found")
|
|
|
|
# Clean up description field
|
|
desc = movie.get('description', '')
|
|
if desc and ('Trạng thái' in desc or 'Năm phát hành' in desc):
|
|
movie['description'] = None
|
|
|
|
# Try to enrich with TMDB data
|
|
try:
|
|
from tmdb_service import tmdb_service
|
|
enriched = await tmdb_service.enrich_movie_data(movie)
|
|
return enriched
|
|
except Exception as tmdb_error:
|
|
logger.warning(f"TMDB enrichment failed: {tmdb_error}")
|
|
return movie
|
|
|
|
except HTTPException:
|
|
raise
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch movie: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch movie: {str(e)}")
|
|
|
|
|
|
@router.get("/home/sections")
|
|
async def get_home_more_sections(
|
|
page: int = Query(1, ge=1),
|
|
view: str = Query('home'),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get paginated sections for homepage OR specific views (infinite scroll)."""
|
|
from category_scraper import PhimMoiChillCategoryScraper
|
|
|
|
scraper = PhimMoiChillCategoryScraper()
|
|
try:
|
|
if view == 'home':
|
|
if page < 2:
|
|
results = []
|
|
else:
|
|
results = await scraper.get_mixed_sections(page)
|
|
else:
|
|
results = await scraper.get_view_sections(view, page)
|
|
|
|
return {"sections": results, "page": page}
|
|
except Exception as e:
|
|
logger.error(f"Error fetching more sections: {e}")
|
|
return {"sections": [], "page": page}
|
|
finally:
|
|
await scraper.close()
|
|
|
|
|
|
@router.get("/category")
|
|
async def get_movies_by_category(
|
|
slug: str = Query(..., description="Category slug"),
|
|
page: int = Query(1, ge=1),
|
|
limit: int = Query(24, ge=1, le=50),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get movies for a specific category"""
|
|
from rophim_scraper import RophimScraper
|
|
|
|
try:
|
|
scraper = RophimScraper()
|
|
try:
|
|
results = await scraper.get_category(slug, page, limit)
|
|
movies = [movie.__dict__ for movie in results]
|
|
|
|
return {
|
|
"movies": movies,
|
|
"category": slug,
|
|
"page": page,
|
|
"total": len(movies)
|
|
}
|
|
finally:
|
|
await scraper.close()
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch category: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch category: {str(e)}")
|
|
|
|
|
|
@router.get("/categories/all")
|
|
async def get_all_categories(authorized: bool = Depends(verify_hmac)):
|
|
"""Get all themed category sections in one call"""
|
|
from category_scraper import get_categories_sync
|
|
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
categories = await loop.run_in_executor(None, get_categories_sync)
|
|
|
|
return {
|
|
"categories": categories,
|
|
"total_sections": len(categories)
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch categories: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch categories: {str(e)}")
|
|
|
|
|
|
@router.get("/categories/hot")
|
|
async def get_hot_category(
|
|
limit: int = Query(24, ge=1, le=50),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get Hot Movies category"""
|
|
from category_scraper import PhimMoiChillCategoryScraper
|
|
|
|
try:
|
|
async def _fetch():
|
|
scraper = PhimMoiChillCategoryScraper()
|
|
try:
|
|
movies = await scraper.get_hot_movies(limit)
|
|
await scraper.close()
|
|
return movies
|
|
except Exception:
|
|
await scraper.close()
|
|
raise
|
|
|
|
loop = asyncio.get_event_loop()
|
|
movies = await loop.run_in_executor(None, lambda: asyncio.run(_fetch()))
|
|
|
|
return {"movies": movies, "category": "hot", "total": len(movies)}
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch hot movies: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch hot movies: {str(e)}")
|
|
|
|
|
|
@router.get("/categories/new-releases")
|
|
async def get_new_releases_category(
|
|
limit: int = Query(24, ge=1, le=50),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get New Releases category"""
|
|
from category_scraper import PhimMoiChillCategoryScraper
|
|
|
|
try:
|
|
async def _fetch():
|
|
scraper = PhimMoiChillCategoryScraper()
|
|
try:
|
|
movies = await scraper.get_new_releases(limit)
|
|
await scraper.close()
|
|
return movies
|
|
except Exception:
|
|
await scraper.close()
|
|
raise
|
|
|
|
loop = asyncio.get_event_loop()
|
|
movies = await loop.run_in_executor(None, lambda: asyncio.run(_fetch()))
|
|
|
|
return {"movies": movies, "category": "new_releases", "total": len(movies)}
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch new releases: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch new releases: {str(e)}")
|
|
|
|
|
|
@router.get("/categories/top10")
|
|
async def get_top10_category(authorized: bool = Depends(verify_hmac)):
|
|
"""Get Top 10 Most Watched"""
|
|
from category_scraper import PhimMoiChillCategoryScraper
|
|
|
|
try:
|
|
async def _fetch():
|
|
scraper = PhimMoiChillCategoryScraper()
|
|
try:
|
|
movies = await scraper.get_top_10()
|
|
await scraper.close()
|
|
return movies
|
|
except Exception:
|
|
await scraper.close()
|
|
raise
|
|
|
|
loop = asyncio.get_event_loop()
|
|
movies = await loop.run_in_executor(None, lambda: asyncio.run(_fetch()))
|
|
|
|
return {"movies": movies, "category": "top10", "total": len(movies)}
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch top 10: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch top 10: {str(e)}")
|
|
|
|
|
|
@router.get("/categories/cinema")
|
|
async def get_cinema_category(
|
|
limit: int = Query(24, ge=1, le=50),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Get Cinema Releases category"""
|
|
from category_scraper import PhimMoiChillCategoryScraper
|
|
|
|
try:
|
|
async def _fetch():
|
|
scraper = PhimMoiChillCategoryScraper()
|
|
try:
|
|
movies = await scraper.get_cinema_releases(limit)
|
|
await scraper.close()
|
|
return movies
|
|
except Exception:
|
|
await scraper.close()
|
|
raise
|
|
|
|
loop = asyncio.get_event_loop()
|
|
movies = await loop.run_in_executor(None, lambda: asyncio.run(_fetch()))
|
|
|
|
return {"movies": movies, "category": "cinema", "total": len(movies)}
|
|
except Exception as e:
|
|
logger.error(f"Failed to fetch cinema releases: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Failed to fetch cinema releases: {str(e)}")
|
|
|
|
|
|
@router.post("/crawl/trigger")
|
|
async def trigger_crawl(
|
|
page: int = Query(1, ge=1),
|
|
limit: int = Query(50, ge=1, le=100),
|
|
authorized: bool = Depends(verify_hmac)
|
|
):
|
|
"""Trigger a movie catalog crawl."""
|
|
from rophim_scraper import get_movies
|
|
|
|
try:
|
|
loop = asyncio.get_event_loop()
|
|
movies = await loop.run_in_executor(None, lambda: get_movies(page, limit))
|
|
|
|
return {
|
|
"success": True,
|
|
"crawled_count": len(movies),
|
|
"page": page,
|
|
"message": f"Successfully crawled {len(movies)} movies"
|
|
}
|
|
except Exception as e:
|
|
logger.error(f"Crawl failed: {e}")
|
|
raise HTTPException(status_code=500, detail=f"Crawl failed: {str(e)}")
|
|
|
|
|
|
@router.get("/crawl/status")
|
|
async def crawl_status():
|
|
"""Get the last crawl status and timestamp"""
|
|
return {
|
|
"status": "ready",
|
|
"message": "Use POST /api/rophim/crawl/trigger to start a crawl"
|
|
}
|