Spaces:
Running
Running
| import uvicorn | |
| import httpx | |
| from fastapi import FastAPI, Response, Request, HTTPException # Added Request | |
| from fastapi.responses import StreamingResponse | |
| from fastapi.middleware.cors import CORSMiddleware | |
| from contextlib import asynccontextmanager | |
| # --- Configuration --- | |
| HEADERS = { | |
| "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36", | |
| "Referer": "https://media.javtrailers.com/", | |
| } | |
| # Global client variable | |
| http_client: httpx.AsyncClient = None | |
| # --- Lifespan Management --- | |
| async def lifespan(app: FastAPI): | |
| global http_client | |
| http_client = httpx.AsyncClient( | |
| headers=HEADERS, | |
| timeout=httpx.Timeout(10.0, read=30.0), | |
| follow_redirects=True | |
| ) | |
| yield | |
| await http_client.aclose() | |
| app = FastAPI(lifespan=lifespan) | |
| # Allow CORS | |
| app.add_middleware( | |
| CORSMiddleware, | |
| allow_origins=["*"], | |
| allow_credentials=True, | |
| allow_methods=["*"], | |
| allow_headers=["*"], | |
| ) | |
| # --- Helpers --- | |
| def rewrite_m3u8_content(content_text: str, base_url: str) -> str: | |
| lines = content_text.splitlines() | |
| new_lines = [] | |
| for line in lines: | |
| line = line.strip() | |
| if not line: | |
| continue | |
| if line.startswith("#"): | |
| new_lines.append(line) | |
| elif line.startswith("http://") or line.startswith("https://"): | |
| new_lines.append(line) | |
| else: | |
| new_lines.append(f"/proxy-segment?base={base_url}&segment={line}") | |
| return "\n".join(new_lines) | |
| async def stream_generator(response: httpx.Response): | |
| """Safely iterates over the stream, handling upstream disconnects.""" | |
| try: | |
| async for chunk in response.aiter_raw(): | |
| yield chunk | |
| except (httpx.ReadError, httpx.RemoteProtocolError, httpx.ConnectError) as e: | |
| print(f"Warning: Stream interrupted by upstream: {e}") | |
| finally: | |
| await response.aclose() | |
| # --- Routes --- | |
| def home(): | |
| return { | |
| "status": "Running", | |
| "usage_hls": "/proxy-playlist?url=...", | |
| "usage_mp4": "/proxy-video?url=..." | |
| } | |
| # --------------------------------------------------------- | |
| # NEW: MP4 Proxy Support (Supports Seeking/Scrubbing) | |
| # --------------------------------------------------------- | |
| async def proxy_video(url: str, request: Request): | |
| try: | |
| # 1. Forward the 'Range' header if the browser sent one. | |
| # This allows the browser to request specific byte chunks (seeking). | |
| req_headers = {} | |
| if "range" in request.headers: | |
| req_headers["Range"] = request.headers["range"] | |
| # 2. Build the request manually | |
| req = http_client.build_request("GET", url, headers=req_headers) | |
| # 3. Send request (stream=True) | |
| r = await http_client.send(req, stream=True) | |
| # 4. Handle errors | |
| if r.status_code >= 400: | |
| await r.aclose() | |
| return Response(content=f"Upstream Error {r.status_code}", status_code=r.status_code) | |
| # 5. Prepare Response Headers | |
| # We must forward Content-Range/Length so the browser knows the video size. | |
| resp_headers = { | |
| "Accept-Ranges": "bytes", # Tell browser we support seeking | |
| "Content-Type": r.headers.get("Content-Type", "video/mp4"), | |
| } | |
| # Only add these if they exist in the upstream response | |
| if "Content-Length" in r.headers: | |
| resp_headers["Content-Length"] = r.headers["Content-Length"] | |
| if "Content-Range" in r.headers: | |
| resp_headers["Content-Range"] = r.headers["Content-Range"] | |
| # 6. Return StreamingResponse | |
| # We pass r.status_code (usually 206 or 200) directly. | |
| return StreamingResponse( | |
| stream_generator(r), | |
| status_code=r.status_code, | |
| headers=resp_headers, | |
| media_type="video/mp4" | |
| ) | |
| except httpx.RequestError as e: | |
| return Response(content=f"Video Error: {e}", status_code=502) | |
| # --- Existing HLS Routes --- | |
| async def proxy_playlist(url: str): | |
| try: | |
| resp = await http_client.get(url) | |
| resp.raise_for_status() | |
| base_url = str(resp.url).rsplit('/', 1)[0] + '/' | |
| modified_m3u8 = rewrite_m3u8_content(resp.text, base_url) | |
| return Response(content=modified_m3u8, media_type="application/vnd.apple.mpegurl") | |
| except httpx.RequestError as e: | |
| return Response(content=f"Proxy Error: {e}", status_code=502) | |
| async def proxy_segment(base: str, segment: str): | |
| full_url = base + segment | |
| # CASE 1: Nested Playlist (.m3u8) | |
| if segment.split('?')[0].endswith('.m3u8'): | |
| try: | |
| resp = await http_client.get(full_url) | |
| resp.raise_for_status() | |
| new_base = str(resp.url).rsplit('/', 1)[0] + '/' | |
| modified_m3u8 = rewrite_m3u8_content(resp.text, new_base) | |
| return Response(content=modified_m3u8, media_type="application/vnd.apple.mpegurl") | |
| except httpx.RequestError as e: | |
| return Response(content=f"Playlist Error: {e}", status_code=502) | |
| # CASE 2: Video Segment (.ts) | |
| try: | |
| req = http_client.build_request("GET", full_url) | |
| r = await http_client.send(req, stream=True) | |
| if r.status_code >= 400: | |
| await r.aclose() | |
| return Response(content=f"Upstream Error {r.status_code}", status_code=r.status_code) | |
| return StreamingResponse( | |
| stream_generator(r), | |
| media_type="video/MP2T" | |
| ) | |
| except httpx.RequestError as e: | |
| print(f"Connection failed for {segment}: {e}") | |
| return Response(content="Segment unavailable", status_code=502) | |
| if __name__ == "__main__": | |
| uvicorn.run(app, host="0.0.0.0", port=7860) |