v0.1.0 — Réécriture complète de OhmStreaming

Nouvelle version réécrite de zéro : recherche multi-sources (Vostfree,
French-Manga), extraction 2 niveaux, proxy vidéo intégré, streaming/téléchargement
HLS, métadonnées Kitsu, bibliothèque locale, comptes JWT + administration,
découverte fusionnée, indexeur Torznab (Sonarr/Prowlarr).
This commit is contained in:
Roman
2026-09-22 10:05:47 +00:00
commit 41566ab5fb
66 changed files with 8895 additions and 0 deletions
+129
View File
@@ -0,0 +1,129 @@
"""Proxy vidéo : sert les flux distants (mp4/HLS) via le serveur.
Indispensable pour les hébergeurs dont les tokens sont liés à l'IP qui a
résolu le lien (le navigateur de l'utilisateur a une IP différente du serveur).
Les playlists m3u8 sont réécrites pour que segments et variantes passent aussi
par le proxy. Accès réservé aux utilisateurs connectés.
"""
import logging
import re
from collections.abc import AsyncIterator
from urllib.parse import quote, urljoin
import httpx
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from fastapi.responses import Response, StreamingResponse
from app.routers.auth import current_user
from app.scrapers.http import get_client
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api", tags=["proxy"], dependencies=[Depends(current_user)])
HLS_CONTENT_TYPES = ("application/vnd.apple.mpegurl", "application/x-mpegurl", "audio/mpegurl")
HOP_HEADERS = (
"content-type", "content-length", "content-range", "accept-ranges", "cache-control"
)
def _check_url(url: str) -> None:
if not url.startswith(("http://", "https://")):
raise HTTPException(422, "URL invalide")
def proxy_url(remote_url: str, referer: str | None = None) -> str:
out = "/api/proxy?url=" + quote(remote_url, safe="")
if referer:
out += "&ref=" + quote(referer, safe="")
return out
@router.get("/proxy")
async def proxy(
request: Request,
url: str = Query(),
ref: str | None = Query(None),
) -> Response:
_check_url(url)
headers: dict[str, str] = {}
if ref:
_check_url(ref)
headers["Referer"] = ref
if range_header := request.headers.get("range"):
headers["Range"] = range_header
client = get_client()
try:
upstream = await client.send(
client.build_request("GET", url, headers=headers), stream=True
)
except httpx.HTTPError as exc:
logger.error("Proxy : échec de connexion à %s : %s", url, exc)
raise HTTPException(502, f"Hébergeur injoignable : {exc}") from exc
content_type = upstream.headers.get("content-type", "")
is_hls = any(t in content_type for t in HLS_CONTENT_TYPES) or url.endswith(".m3u8")
if is_hls:
# Playlist : lecture intégrale puis réécriture des URIs via le proxy
try:
body = (await upstream.aread()).decode("utf-8", errors="replace")
finally:
await upstream.aclose()
if upstream.status_code >= 400:
logger.error("Proxy HLS : %s → HTTP %s", url, upstream.status_code)
raise HTTPException(502, f"Hébergeur : HTTP {upstream.status_code}")
rewritten = _rewrite_playlist(body, url, ref)
return Response(
rewritten,
media_type="application/vnd.apple.mpegurl",
headers={"Cache-Control": "no-store"},
)
if upstream.status_code >= 400:
await upstream.aclose()
logger.error("Proxy : %s → HTTP %s", url, upstream.status_code)
raise HTTPException(502, f"Hébergeur : HTTP {upstream.status_code}")
async def byte_stream() -> AsyncIterator[bytes]:
try:
async for chunk in upstream.aiter_bytes(1 << 16):
yield chunk
finally:
await upstream.aclose()
passthrough = {
h: upstream.headers[h] for h in HOP_HEADERS if h in upstream.headers
}
passthrough.setdefault("accept-ranges", "bytes")
return StreamingResponse(
byte_stream(),
status_code=upstream.status_code,
headers=passthrough,
media_type=content_type.split(";")[0] or "application/octet-stream",
)
_KEY_URI_RE = re.compile(r'URI="([^"]+)"')
def _rewrite_playlist(body: str, base_url: str, referer: str | None) -> str:
"""Réécrit toutes les URIs d'une playlist m3u8 pour passer par le proxy."""
def absolutize(uri: str) -> str:
return proxy_url(urljoin(base_url, uri), referer)
lines = []
for line in body.splitlines():
stripped = line.strip()
if not stripped:
continue
if stripped.startswith("#"):
lines.append(_KEY_URI_RE.sub(
lambda m: f'URI="{absolutize(m.group(1))}"', line
))
else:
lines.append(absolutize(stripped))
return "\n".join(lines) + "\n"