Spaces:
Running
Running
Fix VTV proxy: separate m3u8 variant proxy from segment proxy, fix content-type handling, add verify=False for self-signed certs
Browse files- vtv_api.py +34 -9
vtv_api.py
CHANGED
|
@@ -61,6 +61,9 @@ FPTPLAY_URLS = {
|
|
| 61 |
"vtv6": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
|
| 62 |
}
|
| 63 |
|
|
|
|
|
|
|
|
|
|
| 64 |
# Channels with no public stream available
|
| 65 |
UNAVAILABLE_CHANNELS = {"vtv10", "vtvprime"}
|
| 66 |
|
|
@@ -182,10 +185,12 @@ def proxy_page(url: str = Query(...)):
|
|
| 182 |
|
| 183 |
@router.get("/api/proxy/m3u8/vtv")
|
| 184 |
def proxy_vtv_m3u8(url: str = Query(...)):
|
| 185 |
-
"""Proxy m3u8 playlist
|
|
|
|
| 186 |
try:
|
| 187 |
headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*", "Referer": "https://vtv.vn/"}
|
| 188 |
-
|
|
|
|
| 189 |
r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=verify)
|
| 190 |
if r.status_code != 200:
|
| 191 |
return Response(status_code=502, content="upstream error")
|
|
@@ -193,6 +198,9 @@ def proxy_vtv_m3u8(url: str = Query(...)):
|
|
| 193 |
lines = content.split('\n')
|
| 194 |
rewritten = []
|
| 195 |
base_url = url.rsplit('/', 1)[0] + '/'
|
|
|
|
|
|
|
|
|
|
| 196 |
for line in lines:
|
| 197 |
line = line.strip()
|
| 198 |
if not line or line.startswith('#'):
|
|
@@ -201,25 +209,42 @@ def proxy_vtv_m3u8(url: str = Query(...)):
|
|
| 201 |
seg_url = line
|
| 202 |
if not seg_url.startswith('http'):
|
| 203 |
seg_url = base_url + seg_url
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
except Exception as e:
|
| 207 |
return Response(status_code=502, content="proxy error: " + str(e))
|
| 208 |
|
| 209 |
@router.get("/api/proxy/seg/vtv")
|
| 210 |
def proxy_vtv_segment(url: str = Query(...)):
|
| 211 |
-
"""Proxy video segments (.ts files)."""
|
| 212 |
try:
|
| 213 |
headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*", "Referer": "https://vtv.vn/"}
|
| 214 |
-
|
|
|
|
| 215 |
r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=verify)
|
| 216 |
if r.status_code != 200:
|
| 217 |
return Response(status_code=502, content="upstream error")
|
| 218 |
data = r.content
|
| 219 |
-
#
|
| 220 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
data = data[188:]
|
| 222 |
-
return Response(content=data, media_type=
|
| 223 |
except:
|
| 224 |
return Response(status_code=502, content="proxy error")
|
| 225 |
|
|
|
|
| 61 |
"vtv6": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
|
| 62 |
}
|
| 63 |
|
| 64 |
+
# ===== DEAD: xemtv.net (all 403 from Cloudflare, kept for reference) =====
|
| 65 |
+
# XEMTV_PHP_ENDPOINTS = { ... } # All dead as of 2026-06
|
| 66 |
+
|
| 67 |
# Channels with no public stream available
|
| 68 |
UNAVAILABLE_CHANNELS = {"vtv10", "vtvprime"}
|
| 69 |
|
|
|
|
| 185 |
|
| 186 |
@router.get("/api/proxy/m3u8/vtv")
|
| 187 |
def proxy_vtv_m3u8(url: str = Query(...)):
|
| 188 |
+
"""Proxy m3u8 playlist. Rewrites .ts segments and .m3u8 variants through our proxy chain.
|
| 189 |
+
Uses fully-qualified absolute URLs so HLS.js resolves them correctly."""
|
| 190 |
try:
|
| 191 |
headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*", "Referer": "https://vtv.vn/"}
|
| 192 |
+
# Determine if we need verify=False (self-signed cert on some CDNs)
|
| 193 |
+
verify = "vtvdigital.vn" not in url
|
| 194 |
r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=verify)
|
| 195 |
if r.status_code != 200:
|
| 196 |
return Response(status_code=502, content="upstream error")
|
|
|
|
| 198 |
lines = content.split('\n')
|
| 199 |
rewritten = []
|
| 200 |
base_url = url.rsplit('/', 1)[0] + '/'
|
| 201 |
+
# Build the base URL for this proxy endpoint
|
| 202 |
+
proxy_base = "/api/proxy/m3u8/vtv?url="
|
| 203 |
+
seg_proxy_base = "/api/proxy/seg/vtv?url="
|
| 204 |
for line in lines:
|
| 205 |
line = line.strip()
|
| 206 |
if not line or line.startswith('#'):
|
|
|
|
| 209 |
seg_url = line
|
| 210 |
if not seg_url.startswith('http'):
|
| 211 |
seg_url = base_url + seg_url
|
| 212 |
+
# Distinguish between .m3u8 variant playlists and .ts segments
|
| 213 |
+
if seg_url.endswith('.m3u8'):
|
| 214 |
+
# Variant playlist → proxy through this same m3u8 endpoint (full URL)
|
| 215 |
+
rewritten.append(proxy_base + requests.utils.quote(seg_url, safe=""))
|
| 216 |
+
else:
|
| 217 |
+
# .ts segment → proxy through segment endpoint (full URL)
|
| 218 |
+
rewritten.append(seg_proxy_base + requests.utils.quote(seg_url, safe=""))
|
| 219 |
+
return Response(content='\n'.join(rewritten).encode("utf-8"), media_type="application/vnd.apple.mpegurl", headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache, no-store", "Access-Control-Allow-Headers": "*"})
|
| 220 |
except Exception as e:
|
| 221 |
return Response(status_code=502, content="proxy error: " + str(e))
|
| 222 |
|
| 223 |
@router.get("/api/proxy/seg/vtv")
|
| 224 |
def proxy_vtv_segment(url: str = Query(...)):
|
| 225 |
+
"""Proxy video segments (.ts files) and other binary content."""
|
| 226 |
try:
|
| 227 |
headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*", "Referer": "https://vtv.vn/"}
|
| 228 |
+
# Determine if we need verify=False (self-signed cert on some CDNs)
|
| 229 |
+
verify = "vtvdigital.vn" not in url
|
| 230 |
r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=verify)
|
| 231 |
if r.status_code != 200:
|
| 232 |
return Response(status_code=502, content="upstream error")
|
| 233 |
data = r.content
|
| 234 |
+
# Determine content type from URL
|
| 235 |
+
content_type = "video/mp2t" # default for .ts
|
| 236 |
+
if url.endswith('.m3u8') or url.endswith('.m3u'):
|
| 237 |
+
content_type = "application/vnd.apple.mpegurl"
|
| 238 |
+
elif url.endswith('.mp4'):
|
| 239 |
+
content_type = "video/mp4"
|
| 240 |
+
elif url.endswith('.aac'):
|
| 241 |
+
content_type = "audio/aac"
|
| 242 |
+
elif url.endswith('.webm'):
|
| 243 |
+
content_type = "video/webm"
|
| 244 |
+
# Fix: some TS segments start with PNG header (188 bytes offset)
|
| 245 |
+
if content_type == "video/mp2t" and len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
|
| 246 |
data = data[188:]
|
| 247 |
+
return Response(content=data, media_type=content_type, headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600", "Access-Control-Allow-Headers": "*"})
|
| 248 |
except:
|
| 249 |
return Response(status_code=502, content="proxy error")
|
| 250 |
|