| import logging |
| import time |
| from typing import List, Optional |
| from fastapi import FastAPI, Request, HTTPException, Query |
| from fastapi.responses import JSONResponse, FileResponse, StreamingResponse, RedirectResponse |
| from fastapi.middleware.cors import CORSMiddleware |
| from fastapi.middleware.gzip import GZipMiddleware |
| import httpx |
| from scraper.engine import scraper |
| from downloader import downloader |
| import os |
| import re |
| from urllib.parse import unquote, quote |
| from fastapi.staticfiles import StaticFiles |
| from database import init_db |
| from keep_alive import keep_alive |
| import asyncio |
| import io |
|
|
| |
| logging.basicConfig( |
| level=logging.INFO, |
| format="%(asctime)s [%(levelname)s] %(name)s: %(message)s", |
| datefmt="%Y-%m-%d %H:%M:%S", |
| ) |
| logger = logging.getLogger("backend") |
|
|
| app = FastAPI(title="MEIH Movies API", version="2.0.0") |
|
|
| |
| class MemoryCache: |
| def __init__(self): |
| self._cache = {} |
|
|
| def get(self, key: str): |
| item = self._cache.get(key) |
| if item: |
| expire_time, data = item |
| if time.time() < expire_time: |
| return data |
| else: |
| del self._cache[key] |
| return None |
|
|
| def set(self, key: str, data, ttl_seconds: int = 600): |
| self._cache[key] = (time.time() + ttl_seconds, data) |
|
|
| cache = MemoryCache() |
|
|
| async def warm_scraper(): |
| """Warms up the scraper by making an initial request to sync cookies.""" |
| logger.info("🔥 Warming up scraper in background...") |
| try: |
| |
| await asyncio.sleep(5) |
| await scraper.fetch_home(page=1) |
| logger.info("✅ Scraper warmed up and cookies synced") |
| except Exception as e: |
| logger.warning(f"⚠️ Scraper warmup failed (will retry on first request): {e}") |
|
|
| @app.on_event("startup") |
| async def startup_event(): |
| await init_db() |
| logger.info("🚀 Database initialized and ready") |
| |
| |
| is_hf = os.environ.get("SPACE_ID") is not None or os.environ.get("HF_SPACE") is not None |
| |
| if not is_hf: |
| |
| asyncio.create_task(keep_alive.start()) |
| |
| asyncio.create_task(warm_scraper()) |
| |
| if hasattr(scraper, '_turbo_prefetch'): |
| asyncio.create_task(scraper._turbo_prefetch()) |
| logger.info("🔄 Background services activated") |
| else: |
| logger.info("🤗 Running on Hugging Face - Lightweight mode enabled") |
| |
| asyncio.create_task(warm_scraper()) |
|
|
|
|
| |
| app.add_middleware( |
| CORSMiddleware, |
| allow_origins=["*"], |
| allow_credentials=True, |
| allow_methods=["*"], |
| allow_headers=["*"], |
| ) |
| app.add_middleware(GZipMiddleware, minimum_size=1000) |
|
|
| @app.get("/") |
| async def root(): |
| return {"status": "ok", "message": "MEIH Movies API is running"} |
|
|
| @app.get("/health") |
| async def health(): |
| return {"status": "online", "timestamp": time.time()} |
|
|
| @app.get("/latest") |
| async def get_latest(page: int = 1): |
| cache_key = f"latest_{page}" |
| cached = cache.get(cache_key) |
| if cached: |
| return cached |
| |
| try: |
| items = await scraper.fetch_home(page=page) |
| if items: |
| cache.set(cache_key, items) |
| return items |
| except Exception as e: |
| logger.error(f"Error fetching latest: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/category/{cat_id}") |
| async def get_category(cat_id: str, page: int = 1): |
| cache_key = f"cat_{cat_id}_{page}" |
| cached = cache.get(cache_key) |
| if cached: |
| return cached |
| |
| try: |
| items = await scraper.fetch_category(cat_id, page=page) |
| if items: |
| cache.set(cache_key, items) |
| return items |
| except Exception as e: |
| logger.error(f"Error fetching category {cat_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/search") |
| async def search(q: str): |
| cache_key = f"search_{q}" |
| cached = cache.get(cache_key) |
| if cached: |
| return cached |
| |
| try: |
| items = await scraper.search(q) |
| if items: |
| cache.set(cache_key, items, ttl_seconds=3600) |
| return items |
| except Exception as e: |
| logger.error(f"Error searching for {q}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/details/{safe_id}") |
| async def get_details(safe_id: str): |
| cache_key = f"details_{safe_id}" |
| cached = cache.get(cache_key) |
| if cached: |
| return cached |
| |
| try: |
| details = await scraper.fetch_details(safe_id) |
| if not details: |
| return JSONResponse(status_code=404, content={"error": "Content not found"}) |
| |
| cache.set(cache_key, details, ttl_seconds=86400) |
| return details |
| except Exception as e: |
| logger.error(f"Error fetching details for {safe_id}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
| @app.get("/proxy/image") |
| async def proxy_image(url: str): |
| if not url: |
| raise HTTPException(status_code=400, detail="URL is required") |
| |
| url = unquote(url) |
| |
| |
| cache_dir = os.path.join(base_dir, "cache", "images") |
| os.makedirs(cache_dir, exist_ok=True) |
| |
| |
| import hashlib |
| url_hash = hashlib.md5(url.encode()).hexdigest() |
| cache_path = os.path.join(cache_dir, f"{url_hash}.img") |
| |
| |
| if os.path.exists(cache_path): |
| |
| if time.time() - os.path.getmtime(cache_path) < 604800: |
| return FileResponse( |
| cache_path, |
| media_type="image/jpeg", |
| headers={"Cache-Control": "public, max-age=31536000"} |
| ) |
|
|
| try: |
| |
| async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client: |
| resp = await client.get(url, headers={"User-Agent": scraper.headers["User-Agent"]}) |
| if resp.status_code == 200: |
| |
| content = resp.content |
| with open(cache_path, "wb") as f: |
| f.write(content) |
| |
| |
| return StreamingResponse( |
| io.BytesIO(content), |
| media_type=resp.headers.get("Content-Type", "image/jpeg"), |
| headers={"Cache-Control": "public, max-age=31536000"} |
| ) |
| else: |
| logger.warning(f"Failed to proxy image {url} (Status: {resp.status_code})") |
| return JSONResponse(status_code=resp.status_code, content={"error": f"Failed (Status {resp.status_code})"}) |
| except httpx.TimeoutException: |
| logger.warning(f"Timeout proxying image: {url}") |
| return JSONResponse(status_code=504, content={"error": "Image timeout"}) |
| except Exception as e: |
| logger.error(f"Proxy image error for {url}: {type(e).__name__} - {str(e)}") |
| return JSONResponse(status_code=500, content={"error": str(e)}) |
|
|
| @app.get("/download/info") |
| async def get_download_info(url: str): |
| try: |
| info = await downloader.get_info(url) |
| return info |
| except Exception as e: |
| logger.error(f"Download info error for {url}: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
|
|
| @app.get("/download/file") |
| async def download_file(url: str, filename: str = "video.mp4"): |
| """Handles file downloads, proxying if necessary to bypass IP blocks or hotlink protection.""" |
| if not url: |
| raise HTTPException(status_code=400, detail="URL is required") |
| |
| url = unquote(url) |
| |
| |
| proxy_domains = [ |
| "googlevideo.com", |
| "manifest.googlevideo.com", |
| "larozavideo.net", |
| "larooza.site", |
| "larooza.mom", |
| "laroza-tv.net", |
| "youtube.com", |
| "youtu.be" |
| ] |
| |
| should_proxy = any(domain in url for domain in proxy_domains) |
| |
| if should_proxy: |
| logger.info(f"🛡️ Proxying download: {filename[:50]}...") |
| |
| |
| |
| ascii_filename = re.sub(r'[^\x00-\x7F]+', '_', filename) |
| encoded_filename = quote(filename) |
| |
| async def stream_generator(): |
| async with httpx.AsyncClient(timeout=None, follow_redirects=True) as client: |
| try: |
| async with client.stream("GET", url, headers={"User-Agent": scraper.headers["User-Agent"]}) as resp: |
| if resp.status_code != 200: |
| logger.error(f"Proxy source returned {resp.status_code}") |
| return |
|
|
| |
| |
| async for chunk in resp.aiter_bytes(chunk_size=1024*1024): |
| yield chunk |
| except Exception as e: |
| logger.error(f"Streaming error: {e}") |
|
|
| |
| try: |
| async with httpx.AsyncClient(timeout=10.0, follow_redirects=True) as client: |
| head_resp = await client.head(url, headers={"User-Agent": scraper.headers["User-Agent"]}) |
| content_length = head_resp.headers.get("Content-Length") |
| content_type = head_resp.headers.get("Content-Type", "video/mp4") |
| except: |
| content_length = None |
| content_type = "video/mp4" |
|
|
| headers = { |
| "Content-Disposition": f"attachment; filename=\"{ascii_filename}\"; filename*=UTF-8''{encoded_filename}", |
| "Access-Control-Expose-Headers": "Content-Disposition" |
| } |
| if content_length: |
| headers["Content-Length"] = content_length |
|
|
| return StreamingResponse(stream_generator(), media_type=content_type, headers=headers) |
| |
| |
| return RedirectResponse(url=url) |
|
|
| @app.get("/health") |
| async def health(): |
| |
| fs_status = "OFFLINE" |
| try: |
| |
| async with httpx.AsyncClient(timeout=5.0) as client: |
| resp = await client.get("http://localhost:8191/health") |
| if resp.status_code == 200: |
| fs_status = "ONLINE" |
| except: |
| pass |
|
|
| return { |
| "backend": "ONLINE", |
| "flaresolverr": fs_status, |
| "scraper_sync": scraper._cookies_synced, |
| "timestamp": time.time() |
| } |
|
|
| |
| |
| |
| base_dir = os.path.dirname(__file__) |
| frontend_path = os.path.join(base_dir, "meih-netflix-clone", "dist") |
|
|
| if not os.path.exists(frontend_path): |
| |
| frontend_path = os.path.join(base_dir, "..", "meih-netflix-clone", "dist") |
|
|
| if os.path.exists(frontend_path): |
| |
| assets_path = os.path.join(frontend_path, "assets") |
| if os.path.exists(assets_path): |
| app.mount("/assets", StaticFiles(directory=assets_path), name="assets") |
| |
| @app.get("/{full_path:path}") |
| async def serve_frontend(full_path: str): |
| |
| if full_path.startswith(("api/", "latest", "category/", "search", "details", "proxy", "download", "health")): |
| return JSONResponse(status_code=404, content={"error": "Not Found"}) |
| |
| |
| file_path = os.path.join(frontend_path, full_path) |
| if os.path.exists(file_path) and os.path.isfile(file_path): |
| return FileResponse(file_path) |
| return FileResponse(os.path.join(frontend_path, "index.html")) |
| else: |
| logger.warning(f"Frontend dist folder not found at {frontend_path}. Frontend serving disabled.") |
|
|
| if __name__ == "__main__": |
| import uvicorn |
| |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|