import asyncio import urllib.parse from fastapi import FastAPI, Query, Request, Response from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import StreamingResponse import scraper5 as scraper import uvicorn from contextlib import asynccontextmanager import httpx # SOCKS5 tunel cez Xray (Bratislava SK) XRAY_PROXY = "socks5://127.0.0.1:10808" @asynccontextmanager async def lifespan(app: FastAPI): print("[STARTUP] Inicializujem perzistentný prehliadač...") asyncio.create_task(scraper.manager.start()) yield print("[SHUTDOWN] Čistenie...") app = FastAPI(title="Movie API Backend", lifespan=lifespan) app.add_middleware( CORSMiddleware, allow_origins=["*"], allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) # --------------------------------------------------------------------------- # Pomocné funkcie pre proxy # --------------------------------------------------------------------------- def make_proxy_url(request: Request, target_url: str) -> str: """Vytvorí URL na náš /proxy endpoint pre danú target_url.""" encoded = urllib.parse.quote(target_url, safe="") base = str(request.base_url).rstrip("/") return f"{base}/proxy?url={encoded}" def rewrite_m3u8(content: str, master_url: str, request: Request) -> str: """ Prepíše všetky URL v M3U8 playliste tak, aby šli cez náš /proxy endpoint. Relatívne URL sú absolutizované voči master_url. """ base = master_url.rsplit("/", 1)[0] + "/" lines = content.splitlines() result = [] for line in lines: stripped = line.strip() if not stripped: result.append(stripped) elif stripped.startswith("#"): # Direktívy – prepíšeme len URI= atribúty (napr. AES-128 kľúče) if 'URI="' in stripped: start = stripped.index('URI="') + 5 end = stripped.index('"', start) seg_url = stripped[start:end] if not seg_url.startswith("http"): seg_url = base + seg_url proxy_seg = make_proxy_url(request, seg_url) stripped = stripped[:start] + proxy_seg + stripped[end:] result.append(stripped) else: # Segment URL alebo sub-playlist if not stripped.startswith("http"): stripped = base + stripped result.append(make_proxy_url(request, stripped)) return "\n".join(result) # --------------------------------------------------------------------------- # /proxy – Stream proxy (prechádza cez Xray SK tunel) # --------------------------------------------------------------------------- @app.get("/proxy") async def proxy_stream(request: Request, url: str = Query(...)): """ Stiahne URL cez Xray SOCKS5 tunel (SK IP) a streamuje obsah klientovi. Pre .m3u8 prepíše interné URL na proxy verzie, aby celý stream šiel tunelom. """ headers = { "User-Agent": ( "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/122.0.0.0 Safari/537.36" ), "Referer": "https://mrkaj.me/", "Origin": "https://mrkaj.me", } try: async with httpx.AsyncClient( proxy=XRAY_PROXY, timeout=30, verify=False ) as client: async with client.stream("GET", url, headers=headers) as r: content_type = r.headers.get("content-type", "application/octet-stream") is_m3u8 = ( "mpegurl" in content_type.lower() or url.split("?")[0].endswith(".m3u8") ) if is_m3u8: # M3U8 – prečítame celý a prepíšeme URL segmentov raw = await r.aread() text = raw.decode("utf-8", errors="replace") rewritten = rewrite_m3u8(text, url, request) print(f"[PROXY] M3U8 prepísaný: {url[:70]}...") return Response( content=rewritten, media_type="application/vnd.apple.mpegurl", headers={ "Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache", }, ) else: # Binárny segment (TS, mp4, kľúče) – streamujeme po chunkoch print(f"[PROXY] Segment: {url[:70]}...") async def stream_chunks(): async for chunk in r.aiter_bytes(chunk_size=32768): yield chunk return StreamingResponse( stream_chunks(), media_type=content_type, headers={ "Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache", }, ) except httpx.ProxyError as e: print(f"[PROXY ERROR] Xray tunel nefunguje: {e}") return Response(status_code=502, content=b"Bad Gateway - Xray tunnel down") except Exception as e: print(f"[PROXY ERROR] {e}") return Response(status_code=500, content=str(e).encode()) # --------------------------------------------------------------------------- # Ostatné endpointy # --------------------------------------------------------------------------- @app.get("/screenshot") async def screenshot(): img_bytes = await scraper.manager.get_screenshot() if img_bytes: return Response(content=img_bytes, media_type="image/png") return {"error": "Zatiaľ nie je otvorená žiadna stránka"} @app.get("/") async def root(): status = "running" if scraper.manager._setup_done else "initializing" return {"status": "online", "browser_status": status} @app.get("/search") async def search(q: str = Query(..., description="Search query")): return await scraper.search_movies(q) @app.get("/details") async def details(slug: str, type: str = "movie"): return await scraper.get_details(slug, type) @app.get("/stream") async def stream_info( request: Request, slug: str, type: str = "movie", s: str = Query(None), e: str = Query(None), lng: str = None, source: int = 0, ): season = int(s) if s and s.isdigit() else None episode = int(e) if e and e.isdigit() else None result = await scraper.get_stream_url( slug, type, season=season, episode=episode, lang=lng, source_idx=source ) if result["stream"]: raw_m3u8 = result["stream"] # Obalíme stream URL do nášho proxy endpointu proxied_m3u8 = make_proxy_url(request, raw_m3u8) proxied_vtt = make_proxy_url(request, result["vtt"]) if result.get("vtt") else None return { "success": True, "stream": proxied_m3u8, # Frontend dostane proxy URL "vtt": proxied_vtt, "raw_stream": raw_m3u8, # Pôvodný link (pre debug) } return {"success": False, "error": "Stream not found by scraper"} if __name__ == "__main__": uvicorn.run(app, host="0.0.0.0", port=7860)