""" Video streaming routes for Ohm Stream Downloader API. """ import os import re from pathlib import Path from fastapi import APIRouter, HTTPException, Request from fastapi.responses import Response, StreamingResponse from app.models import DownloadStatus router = APIRouter(tags=["player"]) def get_download_manager(): from main import download_manager return download_manager def get_templates(): from main import templates return templates @router.get("/video/{task_id}") async def stream_video(task_id: str, request: Request): """Stream a video file with Range support for seeking""" download_manager = get_download_manager() task = download_manager.get_task(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") if task.status != DownloadStatus.COMPLETED: raise HTTPException(status_code=400, detail="Download not completed") if not task.file_path or not os.path.exists(task.file_path): raise HTTPException(status_code=404, detail="File not found") file_path = Path(task.file_path) file_size = file_path.stat().st_size range_header = request.headers.get("range") headers = { "Accept-Ranges": "bytes", "Content-Type": "video/mp4", } if range_header: try: range_match = re.match(r"bytes=(\d+)-(\d*)", range_header) start = int(range_match.group(1)) end = int(range_match.group(2)) if range_match.group(2) else file_size - 1 if start >= file_size or end >= file_size or start > end: headers["Content-Range"] = f"bytes */{file_size}" return Response( status_code=416, headers=headers, content="Requested Range Not Satisfiable", ) content_length = end - start + 1 headers["Content-Range"] = f"bytes {start}-{end}/{file_size}" headers["Content-Length"] = str(content_length) def video_range_reader(): with open(file_path, "rb") as f: f.seek(start) remaining = content_length while remaining > 0: chunk_size = min(1024 * 1024, remaining) data = f.read(chunk_size) if not data: break remaining -= len(data) yield data return Response( content=video_range_reader(), status_code=206, headers=headers ) except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid Range header: {e}") else: def video_reader(): with open(file_path, "rb") as f: while True: data = f.read(1024 * 1024) if not data: break yield data headers["Content-Length"] = str(file_size) return Response(content=video_reader(), headers=headers) @router.get("/stream/{filename}") async def stream_video_by_filename(filename: str, request: Request): """Stream a video file by filename with Range support""" filename = os.path.basename(filename) file_path = Path("downloads") / filename if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") file_size = file_path.stat().st_size range_header = request.headers.get("range") if range_header: try: range_match = re.match(r"bytes=(\d+)-(\d*)", range_header) start = int(range_match.group(1)) end = int(range_match.group(2)) if range_match.group(2) else file_size - 1 if start >= file_size or end >= file_size or start > end: return Response( status_code=416, headers={ "Content-Range": f"bytes */{file_size}", "Accept-Ranges": "bytes", }, content="Requested Range Not Satisfiable", ) content_length = end - start + 1 def video_range_reader(): with open(file_path, "rb") as f: f.seek(start) remaining = content_length while remaining > 0: chunk_size = min(1024 * 1024, remaining) data = f.read(chunk_size) if not data: break remaining -= len(data) yield data return StreamingResponse( video_range_reader(), status_code=206, headers={ "Content-Range": f"bytes {start}-{end}/{file_size}", "Content-Length": str(content_length), "Accept-Ranges": "bytes", "Content-Type": "video/mp4", }, ) except Exception as e: raise HTTPException(status_code=400, detail=f"Invalid Range header: {e}") else: def video_reader(): with open(file_path, "rb") as f: while True: data = f.read(1024 * 1024) if not data: break yield data return StreamingResponse( video_reader(), headers={ "Content-Length": str(file_size), "Accept-Ranges": "bytes", "Content-Type": "video/mp4", }, ) @router.get("/player/{task_id}") async def video_player(request: Request, task_id: str): """Video player page for watching downloaded anime""" from main import download_manager, templates task = download_manager.get_task(task_id) if not task: raise HTTPException(status_code=404, detail="Task not found") if task.status != DownloadStatus.COMPLETED: raise HTTPException(status_code=400, detail="Download not completed") if not task.file_path or not os.path.exists(task.file_path): raise HTTPException(status_code=404, detail="File not found") file_path = Path(task.file_path) file_size = file_path.stat().st_size estimated_duration_seconds = int(file_size / (1.5 * 1024 * 1024)) return templates.TemplateResponse( "player.html", { "request": request, "task_id": task_id, "filename": task.filename, "file_size": file_size, "estimated_duration": estimated_duration_seconds, }, ) @router.get("/watch/{filename}") async def video_player_by_filename(request: Request, filename: str): """Video player page for watching downloaded anime by filename""" from main import templates from app.utils import is_safe_filename, sanitize_filename filename = sanitize_filename(filename) if not is_safe_filename(filename): raise HTTPException( status_code=400, detail="Invalid filename. Path traversal attempts are not allowed.", ) file_path = Path("downloads") / filename if not file_path.exists(): raise HTTPException(status_code=404, detail="File not found") file_size = file_path.stat().st_size estimated_duration_seconds = int(file_size / (1.5 * 1024 * 1024)) return templates.TemplateResponse( "player.html", { "request": request, "task_id": filename, "filename": filename, "file_size": file_size, "estimated_duration": estimated_duration_seconds, }, )