from urllib.parse import quote, urljoin import httpx from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import Response from sqlalchemy.ext.asyncio import AsyncSession from app.db.session import get_db from app.services.tv_streams import get_public_tv_payload, is_allowed_tv_proxy_url router = APIRouter() @router.get("/streams") async def list_public_tv_streams( db: AsyncSession = Depends(get_db), ): return await get_public_tv_payload(db) @router.get("/proxy") async def proxy_tv_stream( url: str = Query(..., description="Upstream TV stream or manifest URL"), db: AsyncSession = Depends(get_db), ): payload = await get_public_tv_payload(db) if not is_allowed_tv_proxy_url(url, payload.get("sources", [])): raise HTTPException(status_code=403, detail="TV proxy target is not allowed") try: async with httpx.AsyncClient(follow_redirects=True, timeout=20.0) as client: upstream = await client.get( url, headers={ "User-Agent": "Mozilla/5.0", "Referer": "https://tv.cctv.com/live/cctv4/", }, ) upstream.raise_for_status() except httpx.HTTPError as exc: raise HTTPException(status_code=502, detail=f"Failed to fetch TV stream: {exc}") from exc content_type = upstream.headers.get("content-type", "application/octet-stream") raw_content = upstream.content response_url = str(upstream.url) is_manifest = ( response_url.endswith(".m3u8") or "mpegurl" in content_type.lower() or raw_content.lstrip().startswith(b"#EXTM3U") ) headers = {"Cache-Control": "no-store"} if is_manifest: manifest_text = upstream.text rewritten_lines: list[str] = [] for line in manifest_text.splitlines(): stripped = line.strip() if not stripped or stripped.startswith("#"): rewritten_lines.append(line) continue absolute_url = urljoin(response_url, stripped) rewritten_lines.append(f"/api/v1/tv/proxy?url={quote(absolute_url, safe='')}") return Response( content="\n".join(rewritten_lines), media_type="application/vnd.apple.mpegurl", headers=headers, ) return Response(content=raw_content, media_type=content_type, headers=headers)