""" 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 from app.downloaders import get_downloader @router.get("/api/player/embed") async def get_player_embed(request: Request, url: str): """ Get an embedded video player for a given episode URL. This route extracts the direct video link and returns an HTML fragment. """ from main import templates try: # 1. Get the downloader for the anime site (e.g. Anime-Sama) downloader = get_downloader(url) if not downloader: raise HTTPException(status_code=400, detail="No downloader found for this URL") # 2. Get the video player URL (embed URL like VidMoly, DoodStream, etc.) video_url, _ = await downloader.get_download_link(url) # 3. Get the direct video file link from the player player_handler = get_downloader(video_url) if not player_handler: # If no direct extractor, we might have to use an iframe return templates.TemplateResponse( "components/player_embed.html", { "request": request, "video_url": video_url, "is_iframe": True } ) direct_url, filename = await player_handler.get_download_link(video_url) return templates.TemplateResponse( "components/player_embed.html", { "request": request, "video_url": direct_url, "filename": filename, "is_iframe": False } ) except Exception as e: import logging logging.getLogger(__name__).error(f"Embed error: {e}", exc_info=True) return f"
Erreur lors de l'extraction de la vidéo : {str(e)}
" @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, }, )