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