Files
ohm_streaming/app/routers/router_player.py
T
root d4d8d8a3b6
CI / Test (Python 3.11) (push) Has been cancelled
CI / Test (Python 3.12) (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Type Check (push) Has been cancelled
CI / Summary (push) Has been cancelled
refactor: migrate main.py to modular routers and add project roadmap
- Migrated monolithic main.py to feature-scoped routers in app/routers/
- Added GEMINI.md for project context and AI instructional guidelines
- Updated README.md with a comprehensive modernization plan (SQL migration, robust scraping DSL, frontend modernization)
- Improved authentication with cookie support and modular JS
- Updated test suite and documentation
2026-03-24 10:12:04 +00:00

239 lines
7.6 KiB
Python

"""
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,
},
)