| 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 |
|
|
| |
| 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=["*"], |
| ) |
|
|
| |
| |
| |
|
|
| 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("#"): |
| |
| 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: |
| |
| if not stripped.startswith("http"): |
| stripped = base + stripped |
| result.append(make_proxy_url(request, stripped)) |
|
|
| return "\n".join(result) |
|
|
| |
| |
| |
|
|
| @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: |
| |
| 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: |
| |
| 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()) |
|
|
| |
| |
| |
|
|
| @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"] |
| |
| 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, |
| "vtt": proxied_vtt, |
| "raw_stream": raw_m3u8, |
| } |
|
|
| return {"success": False, "error": "Stream not found by scraper"} |
|
|
| if __name__ == "__main__": |
| uvicorn.run(app, host="0.0.0.0", port=7860) |
|
|