90 lines
2.8 KiB
Python
90 lines
2.8 KiB
Python
from urllib.parse import urlsplit
|
|
|
|
import httpx
|
|
from fastapi import APIRouter, HTTPException
|
|
from fastapi.responses import Response, StreamingResponse
|
|
from pydantic import BaseModel
|
|
|
|
from server.schemas import DeviceBody, device_payload
|
|
from server.services.device import DeviceProfile
|
|
from server.services.http import http
|
|
from server.services import playlist as playlists
|
|
from server.services.sessions import sessions
|
|
|
|
router = APIRouter(prefix="/api/play")
|
|
|
|
|
|
class PlayBody(BaseModel):
|
|
token: str = ""
|
|
device: DeviceBody | None = None
|
|
vid: int
|
|
|
|
|
|
def _device(token: str, raw: dict | None) -> DeviceProfile | None:
|
|
if not token:
|
|
return None
|
|
if raw:
|
|
return sessions.bind(token, DeviceProfile.merge(raw))
|
|
return sessions.device_for(token)
|
|
|
|
|
|
def _public_url(url: str) -> str:
|
|
parts = urlsplit(url)
|
|
if parts.scheme not in ("http", "https") or not parts.hostname:
|
|
raise HTTPException(status_code=400, detail="播放地址无效")
|
|
return url
|
|
|
|
|
|
@router.post("/episodes")
|
|
def list_episodes(body: PlayBody):
|
|
device = _device(body.token, device_payload(body.device))
|
|
try:
|
|
rows = playlists.load_episodes(body.token, device, body.vid)
|
|
except RuntimeError as exc:
|
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
|
return {
|
|
"episodes": [
|
|
{
|
|
"cid": ep["cid"],
|
|
"idx": ep["idx"],
|
|
"title": ep["title"],
|
|
"playable": bool(ep.get("src")),
|
|
}
|
|
for ep in rows
|
|
]
|
|
}
|
|
|
|
|
|
@router.get("/segment")
|
|
def play_segment(url: str):
|
|
target = _public_url(url)
|
|
try:
|
|
upstream = http.open_stream(target)
|
|
except (httpx.HTTPError, TimeoutError, OSError, RuntimeError) as exc:
|
|
raise HTTPException(status_code=502, detail="分片读取失败") from exc
|
|
media = upstream.headers.get("content-type", "video/mp2t").split(";", 1)[0].strip() or "video/mp2t"
|
|
|
|
def chunks():
|
|
try:
|
|
for chunk in upstream.iter_bytes(64 * 1024):
|
|
if chunk:
|
|
yield chunk
|
|
finally:
|
|
upstream.close()
|
|
|
|
return StreamingResponse(chunks(), media_type=media)
|
|
|
|
|
|
@router.get("/{vid}/{cid}/index.m3u8")
|
|
def episode_playlist(vid: int, cid: int):
|
|
token = sessions.active_token()
|
|
device = sessions.device_for(token) if token else None
|
|
src = playlists.episode_src(token, device, vid, cid)
|
|
if not src:
|
|
raise HTTPException(status_code=404, detail="这一集还没有可播放的地址")
|
|
try:
|
|
text = playlists.playback_playlist(src)
|
|
except RuntimeError as exc:
|
|
raise HTTPException(status_code=502, detail=str(exc)) from exc
|
|
return Response(text, media_type="application/vnd.apple.mpegurl", headers={"Cache-Control": "no-store"})
|