bep40 commited on
Commit
243c3f3
·
verified ·
1 Parent(s): 0c0140d

VTV Digital CDN primary source for VTV1-VTV9 streams

Browse files
Files changed (1) hide show
  1. vtv_api.py +78 -249
vtv_api.py CHANGED
@@ -1,8 +1,7 @@
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
- Fetches stream URLs from xemtv.us PHP endpoints (primary)
4
- Alternative: xemtivitop.com (backup)
5
- Fallback: FPTPlay CDN → VTVGo CDN → xemtv.net (legacy)
6
  EPG data scraped from https://vtv.vn/lich-phat-song.htm
7
  """
8
  import re, time, threading
@@ -21,7 +20,20 @@ UA = {
21
  "Accept-Language": "vi-VN,vi;q=0.9",
22
  }
23
 
24
- # ===== PRIMARY: xemtv.us (new domain, works 2025-2026) =====
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  XEMTV_US_ENDPOINTS = {
26
  "vtv1": "https://xemtv.us/tv/vtv1.php",
27
  "vtv2": "https://xemtv.us/tv/vtv2.php",
@@ -36,7 +48,7 @@ XEMTV_US_ENDPOINTS = {
36
  "vtvprime": "https://xemtv.us/tv/vtvprime.php",
37
  }
38
 
39
- # ===== ALTERNATIVE: xemtivitop.com (backup source) =====
40
  XEMTIVITOP_ENDPOINTS = {
41
  "vtv1": "https://www.xemtivitop.com/2018/10/vtv1-online.html?m=1",
42
  "vtv2": "https://www.xemtivitop.com/2018/10/vtv2-online.html?m=1",
@@ -50,7 +62,7 @@ XEMTIVITOP_ENDPOINTS = {
50
  "vtv10": "https://www.xemtivitop.com/2020/05/vtv10-can-tho-online.html?m=1",
51
  }
52
 
53
- # ===== LEGACY: xemtv.net (may return 403, keep as last resort) =====
54
  XEMTV_LEGACY_ENDPOINTS = {
55
  "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
56
  "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
@@ -66,20 +78,12 @@ XEMTV_LEGACY_ENDPOINTS = {
66
  }
67
 
68
  CHANNEL_NAMES = {
69
- "vtv1": "VTV1",
70
- "vtv2": "VTV2",
71
- "vtv3": "VTV3",
72
- "vtv4": "VTV4",
73
- "vtv5": "VTV5",
74
- "vtv6": "VTV6",
75
- "vtv7": "VTV7",
76
- "vtv8": "VTV8",
77
- "vtv9": "VTV9",
78
- "vtv10": "VTV10",
79
- "vtvprime": "VTVPrime",
80
  }
81
 
82
- # ===== FALLBACK 1: FPTPlay CDN (new URLs 2025-2026) =====
83
  FPTPLAY_URLS = {
84
  "vtv1": "https://live-a.fptplay53.net/live/media/vtv1/live247-hls-avc/index.m3u8",
85
  "vtv2": "https://live-a.fptplay53.net/live/media/vtv2/live247-hls-avc/index.m3u8",
@@ -93,7 +97,7 @@ FPTPLAY_URLS = {
93
  "vtv10": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
94
  }
95
 
96
- # ===== FALLBACK 2: VTVGo CDN =====
97
  VTVGO_FAILOVER = {
98
  "vtv1": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
99
  "vtv2": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
@@ -151,7 +155,6 @@ def fetch_xemtv_us_stream(channel_id):
151
  return None
152
 
153
  def fetch_xemtivitop_stream(channel_id):
154
- """Fetch stream from xemtivitop.com blogspot pages"""
155
  page_url = XEMTIVITOP_ENDPOINTS.get(channel_id)
156
  if not page_url:
157
  return None
@@ -186,11 +189,7 @@ def fetch_fptplay_stream(channel_id):
186
  if not url:
187
  return None
188
  try:
189
- headers = {
190
- "User-Agent": UA["User-Agent"],
191
- "Referer": "https://fptplay.vn/",
192
- "Origin": "https://fptplay.vn",
193
- }
194
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
195
  if r.status_code == 200 and '#EXTM3U' in r.text[:200]:
196
  return url
@@ -211,34 +210,34 @@ def fetch_vtvgo_stream(channel_id):
211
  pass
212
  return None
213
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
  def normalize_fptplay_url(url):
215
  if not url:
216
  return url
217
  old_to_new = {
218
- "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8":
219
- "https://live-a.fptplay53.net/live/media/vtv1/live247-hls-avc/index.m3u8",
220
- "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8":
221
- "https://live-a.fptplay53.net/live/media/vtv2/live247-hls-avc/index.m3u8",
222
- "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8":
223
- "https://live-a.fptplay53.net/live/media/vtv3/live247-hls-avc/index.m3u8",
224
- "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8":
225
- "https://live-a.fptplay53.net/live/media/vtv4/live247-hls-avc/index.m3u8",
226
- "https://live.fptplay53.net/fnxhd1/vtv5hd_vhls.smil/chunklist.m3u8":
227
- "https://live-a.fptplay53.net/live/media/vtv5/live247-hls-avc/index.m3u8",
228
- "https://live.fptplay53.net/fnxhd1/vtv6hd_vhls.smil/chunklist.m3u8":
229
- "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/index.m3u8",
230
- "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8":
231
- "https://live-a.fptplay53.net/live/media/vtv7/live247-hls-avc/index.m3u8",
232
- "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/c.hunklist.m3u8":
233
- "https://live-a.fptplay53.net/live/media/vtv8/live-hls-avc/index.m3u8",
234
- "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8":
235
- "https://live-a.fptplay53.net/live/media/vtv8/live-hls-avc/index.m3u8",
236
- "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8":
237
- "https://live-a.fptplay53.net/live/media/vtv9/live247-hls-avc/index.m3u8",
238
- "https://live.fptplay53.net/fnxhd1/vtv10hd_vhls.smil/chunklist.m3u8":
239
- "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
240
- "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8":
241
- "https://live-a.fptplay53.net/live/media/vtv5/live247-hls-avc/index.m3u8",
242
  }
243
  return old_to_new.get(url, url)
244
 
@@ -256,38 +255,47 @@ def fetch_vtv_stream(channel_id):
256
  if cached is not None:
257
  return cached
258
 
259
- if channel_id == 'vtvprime':
260
- url = fetch_xemtv_us_stream('vtvprime') or fetch_xemtivitop_stream('vtvprime') or fetch_xemtv_legacy_stream('vtvprime')
 
261
  if url:
262
- url = normalize_fptplay_url(url)
263
- _set_cache(channel_id, url)
264
- return url
 
 
 
 
 
 
265
 
 
266
  url = fetch_xemtv_us_stream(channel_id)
267
  if url:
268
- url = normalize_fptplay_url(url)
269
  _set_cache(channel_id, url)
270
  return url
271
 
 
272
  url = fetch_xemtivitop_stream(channel_id)
273
  if url:
274
- url = normalize_fptplay_url(url)
275
  _set_cache(channel_id, url)
276
  return url
277
 
 
278
  url = fetch_fptplay_stream(channel_id)
279
  if url:
280
  _set_cache(channel_id, url)
281
  return url
282
 
 
283
  url = fetch_vtvgo_stream(channel_id)
284
  if url:
285
  _set_cache(channel_id, url)
286
  return url
287
 
 
288
  url = fetch_xemtv_legacy_stream(channel_id)
289
  if url:
290
- url = normalize_fptplay_url(url)
291
  _set_cache(channel_id, url)
292
  return url
293
 
@@ -313,13 +321,10 @@ def api_vtv_stream(channel_id: str):
313
  def proxy_page(url: str = Query(...)):
314
  try:
315
  headers = {**UA}
316
- if "xemtv.us" in url:
317
- headers["Referer"] = "https://xemtv.us/"
318
- elif "xemtv.net" in url:
319
- headers["Referer"] = "https://hd.xemtv.net/"
320
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
321
- if r.status_code != 200:
322
- return Response(status_code=502, content="upstream error")
323
  return Response(content=r.text.encode("utf-8"), media_type="text/html; charset=utf-8", headers={"Access-Control-Allow-Origin": "*"})
324
  except:
325
  return Response(status_code=502, content="proxy error")
@@ -328,28 +333,20 @@ def proxy_page(url: str = Query(...)):
328
  def proxy_vtv_m3u8(url: str = Query(...)):
329
  try:
330
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
331
- if "fptplay" in url:
332
- headers["Referer"] = "https://fptplay.vn/"
333
- headers["Origin"] = "https://fptplay.vn"
334
- elif "xemtv" in url:
335
- headers["Referer"] = "https://xemtv.us/"
336
- elif "vtvgo" in url or "vtvdigital" in url:
337
- headers["Referer"] = "https://vtvgo.vn/"
338
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
339
- if r.status_code != 200:
340
- return Response(status_code=502, content="upstream error")
341
  content = r.text
342
  lines = content.split('\n')
343
  base_url = url.rsplit('/', 1)[0] + '/' if '/' in url else url
344
  rewritten = []
345
  for line in lines:
346
  line = line.strip()
347
- if not line or line.startswith('#'):
348
- rewritten.append(line)
349
  else:
350
- seg_url = line
351
- if not seg_url.startswith('http'):
352
- seg_url = base_url + seg_url
353
  if seg_url.endswith('.m3u8'):
354
  rewritten.append("/api/proxy/m3u8/vtv?url=" + requests.utils.quote(seg_url, safe=""))
355
  else:
@@ -362,179 +359,11 @@ def proxy_vtv_m3u8(url: str = Query(...)):
362
  def proxy_vtv_segment(url: str = Query(...)):
363
  try:
364
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
365
- if "fptplay" in url:
366
- headers["Referer"] = "https://fptplay.vn/"
367
- headers["Origin"] = "https://fptplay.vn"
368
  r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=False)
369
- if r.status_code != 200:
370
- return Response(status_code=502, content="upstream error")
371
  data = r.content
372
- if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
373
- data = data[188:]
374
  return Response(content=data, media_type="video/mp2t", headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"})
375
  except:
376
- return Response(status_code=502, content="proxy error")
377
-
378
- _epg_cache = {}
379
- _epg_cache_time = 0
380
- _EPG_CACHE_TTL = 600
381
-
382
- VTV_CHANNEL_MAP = {
383
- "vtv1": "vtv1", "vtv2": "vtv2", "vtv3": "vtv3", "vtv4": "vtv4",
384
- "vtv5": "vtv5", "vtv5-tay-nam-bo": "vtv5", "vtv5-tay-nguyen": "vtv5",
385
- "vtv7": "vtv7", "vtv8": "vtv8", "vtv6": "vtv6", "vtv9": "vtv9",
386
- "vtv-can-tho": "vtv10",
387
- }
388
-
389
- def _fetch_epg_from_vtv():
390
- global _epg_cache, _epg_cache_time
391
- now_ts = time.time()
392
- if _epg_cache and now_ts - _epg_cache_time < _EPG_CACHE_TTL:
393
- return _epg_cache
394
- epg_data = {}
395
- now_vn = datetime.now(VN_TZ)
396
- try:
397
- headers = {
398
- "User-Agent": UA["User-Agent"], "Accept-Language": "vi-VN,vi;q=0.9",
399
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
400
- "Referer": "https://vtv.vn/",
401
- }
402
- r = requests.get("https://vtv.vn/lich-phat-song.htm", headers=headers, timeout=20)
403
- if r.status_code != 200:
404
- return epg_data
405
- r.encoding = "utf-8"
406
- soup = BeautifulSoup(r.text, "lxml")
407
- channel_order = []
408
- list_channel = soup.find(class_=re.compile(r'list-channel'))
409
- if list_channel:
410
- for link in list_channel.find_all('a', href=re.compile(r'truyen-hinh-truc-tuyen/([^.]+)\.htm')):
411
- ch_id = re.search(r'truyen-hinh-truc-tuyen/([^.]+)\.htm', link.get('href', ''))
412
- if ch_id:
413
- channel_order.append(ch_id.group(1))
414
- if not channel_order:
415
- for link in soup.find_all('a', href=re.compile(r'truyen-hinh-truc-tuyen/([^.]+)\.htm')):
416
- ch_id = re.search(r'truyen-hinh-truc-tuyen/([^.]+)\.htm', link.get('href', ''))
417
- if ch_id and ch_id.group(1) not in channel_order:
418
- channel_order.append(ch_id.group(1))
419
- prog_containers = soup.find_all('ul', class_=re.compile(r'\bprograms\b'))
420
- for i, container in enumerate(prog_containers):
421
- if i >= len(channel_order):
422
- break
423
- vtv_ch_id = channel_order[i]
424
- our_ch_id = VTV_CHANNEL_MAP.get(vtv_ch_id, vtv_ch_id)
425
- if our_ch_id not in epg_data:
426
- epg_data[our_ch_id] = []
427
- for li in container.find_all('li', class_=re.compile(r'\bprogram\b')):
428
- time_span = li.find('span', class_=re.compile(r'\btime\b'))
429
- title_span = li.find('span', class_=re.compile(r'\btitle\b'))
430
- genre_a = li.find('a', class_=re.compile(r'\bgenre\b'))
431
- time_str = time_span.get_text(strip=True) if time_span else ""
432
- title = ""
433
- if genre_a:
434
- title = genre_a.get_text(strip=True)
435
- if not title and title_span:
436
- title = title_span.get_text(strip=True)
437
- if not time_str or not title:
438
- continue
439
- start_dt = _parse_time(time_str, reference_date=now_vn)
440
- if not start_dt:
441
- continue
442
- epg_data[our_ch_id].append({"time": time_str[:5], "title": title[:80], "start_dt": start_dt})
443
- for ch_id in epg_data:
444
- epg_data[ch_id].sort(key=lambda x: x.get("start_dt") or datetime.min)
445
- seen = set()
446
- unique = []
447
- for p in epg_data[ch_id]:
448
- key = (p["time"], p["title"])
449
- if key not in seen:
450
- seen.add(key)
451
- unique.append(p)
452
- epg_data[ch_id] = unique
453
- except Exception as e:
454
- print(f"EPG vtv.vn error: {e}")
455
- _epg_cache = epg_data
456
- _epg_cache_time = now_ts
457
- return epg_data
458
-
459
- def _parse_time(time_str, reference_date=None):
460
- if not time_str:
461
- return None
462
- time_str = time_str.strip().replace("h", ":").replace("H", ":")
463
- m = re.search(r'(\d{1,2}):(\d{2})', time_str)
464
- if m:
465
- try:
466
- hour, minute = int(m.group(1)), int(m.group(2))
467
- if reference_date:
468
- base_date = reference_date
469
- else:
470
- now_vn = datetime.now(VN_TZ)
471
- base_date = now_vn
472
- from datetime import timedelta
473
- if hour < 5:
474
- if base_date.hour < 5:
475
- tv_date = base_date
476
- else:
477
- tv_date = base_date - timedelta(days=1)
478
- else:
479
- tv_date = base_date
480
- result = tv_date.replace(hour=hour, minute=minute, second=0, microsecond=0)
481
- if result.tzinfo is None:
482
- result = result.replace(tzinfo=VN_TZ)
483
- return result
484
- except:
485
- pass
486
- return None
487
-
488
- def _get_epg_for_channel(channel_id):
489
- epg_data = _fetch_epg_from_vtv()
490
- programmes = epg_data.get(channel_id, [])
491
- if not programmes:
492
- return []
493
- now = datetime.now(VN_TZ)
494
- today = now.date()
495
- result = []
496
- today_programmes = []
497
- for p in programmes:
498
- start_dt = p.get("start_dt")
499
- if start_dt and start_dt.date() == today:
500
- today_programmes.append(p)
501
- if not today_programmes:
502
- today_programmes = programmes
503
- for i, p in enumerate(today_programmes):
504
- start_dt = p.get("start_dt")
505
- stop_dt = None
506
- if i + 1 < len(today_programmes):
507
- stop_dt = today_programmes[i + 1].get("start_dt")
508
- is_now = False
509
- if start_dt:
510
- if stop_dt:
511
- is_now = start_dt <= now < stop_dt
512
- else:
513
- is_now = start_dt <= now
514
- end_time = ""
515
- if stop_dt:
516
- end_time = stop_dt.strftime("%H:%M")
517
- result.append({"time": p["time"], "title": p["title"], "end_time": end_time, "now": is_now})
518
- return result
519
-
520
- @router.get("/api/vtv/epg/{channel_id}")
521
- def api_vtv_epg(channel_id: str):
522
- channel_id = channel_id.lower().strip()
523
- if channel_id not in CHANNEL_NAMES:
524
- return JSONResponse({"error": "channel not found"}, status_code=404)
525
- programs = _get_epg_for_channel(channel_id)
526
- return JSONResponse({
527
- "channel": channel_id, "channel_name": CHANNEL_NAMES.get(channel_id, channel_id),
528
- "date": datetime.now(VN_TZ).strftime("%Y-%m-%d"), "programs": programs,
529
- })
530
-
531
- @router.get("/api/vtv/epg")
532
- def api_vtv_epg_refresh():
533
- global _epg_cache, _epg_cache_time
534
- _epg_cache = {}
535
- _epg_cache_time = 0
536
- epg_data = _fetch_epg_from_vtv()
537
- return JSONResponse({
538
- "status": "refreshed", "channels": len(epg_data),
539
- "total_programmes": sum(len(v) for v in epg_data),
540
- })
 
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
+ Fetches stream URLs from VTV Digital CDN (primary, stable)
4
+ Fallback: xemtv.us → xemtivitop.com → FPTPlay CDN → VTVGo CDN → xemtv.net (legacy)
 
5
  EPG data scraped from https://vtv.vn/lich-phat-song.htm
6
  """
7
  import re, time, threading
 
20
  "Accept-Language": "vi-VN,vi;q=0.9",
21
  }
22
 
23
+ # ===== PRIMARY: VTV Digital CDN (direct, stable, low latency) =====
24
+ VTV_DIGITAL_CDN_URLS = {
25
+ "vtv1": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv1/master.m3u8",
26
+ "vtv2": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv2/master.m3u8",
27
+ "vtv3": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv3/master.m3u8",
28
+ "vtv4": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv4/master.m3u8",
29
+ "vtv5": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv5/master.m3u8",
30
+ "vtv6": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv6/master.m3u8",
31
+ "vtv7": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv7/master.m3u8",
32
+ "vtv8": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv8/master.m3u8",
33
+ "vtv9": "https://vtvgolive-ssaimh.vtvdigital.vn/o-QNezId5ssiM7KaV4MfXQ/1781372128/manifest/vtv9/master.m3u8",
34
+ }
35
+
36
+ # ===== BACKUP: xemtv.us =====
37
  XEMTV_US_ENDPOINTS = {
38
  "vtv1": "https://xemtv.us/tv/vtv1.php",
39
  "vtv2": "https://xemtv.us/tv/vtv2.php",
 
48
  "vtvprime": "https://xemtv.us/tv/vtvprime.php",
49
  }
50
 
51
+ # ===== xemtivitop.com =====
52
  XEMTIVITOP_ENDPOINTS = {
53
  "vtv1": "https://www.xemtivitop.com/2018/10/vtv1-online.html?m=1",
54
  "vtv2": "https://www.xemtivitop.com/2018/10/vtv2-online.html?m=1",
 
62
  "vtv10": "https://www.xemtivitop.com/2020/05/vtv10-can-tho-online.html?m=1",
63
  }
64
 
65
+ # ===== xemtv.net (legacy) =====
66
  XEMTV_LEGACY_ENDPOINTS = {
67
  "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
68
  "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
 
78
  }
79
 
80
  CHANNEL_NAMES = {
81
+ "vtv1": "VTV1", "vtv2": "VTV2", "vtv3": "VTV3", "vtv4": "VTV4",
82
+ "vtv5": "VTV5", "vtv6": "VTV6", "vtv7": "VTV7", "vtv8": "VTV8",
83
+ "vtv9": "VTV9", "vtv10": "VTV10", "vtvprime": "VTVPrime",
 
 
 
 
 
 
 
 
84
  }
85
 
86
+ # ===== FALLBACK: FPTPlay CDN =====
87
  FPTPLAY_URLS = {
88
  "vtv1": "https://live-a.fptplay53.net/live/media/vtv1/live247-hls-avc/index.m3u8",
89
  "vtv2": "https://live-a.fptplay53.net/live/media/vtv2/live247-hls-avc/index.m3u8",
 
97
  "vtv10": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
98
  }
99
 
100
+ # ===== FALLBACK: VTVGo CDN =====
101
  VTVGO_FAILOVER = {
102
  "vtv1": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
103
  "vtv2": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
 
155
  return None
156
 
157
  def fetch_xemtivitop_stream(channel_id):
 
158
  page_url = XEMTIVITOP_ENDPOINTS.get(channel_id)
159
  if not page_url:
160
  return None
 
189
  if not url:
190
  return None
191
  try:
192
+ headers = {"User-Agent": UA["User-Agent"], "Referer": "https://fptplay.vn/", "Origin": "https://fptplay.vn"}
 
 
 
 
193
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
194
  if r.status_code == 200 and '#EXTM3U' in r.text[:200]:
195
  return url
 
210
  pass
211
  return None
212
 
213
+ def verify_vtv_digital_url(url):
214
+ """Verify a VTV Digital CDN URL returns valid HLS content"""
215
+ if not url:
216
+ return None
217
+ try:
218
+ headers = {**UA, "Referer": "https://vtvgo.vn/", "Origin": "https://vtvgo.vn"}
219
+ r = requests.get(url, headers=headers, timeout=10, allow_redirects=True, verify=False)
220
+ if r.status_code == 200 and '#EXTM3U' in r.text[:200]:
221
+ return url
222
+ except:
223
+ pass
224
+ return None
225
+
226
  def normalize_fptplay_url(url):
227
  if not url:
228
  return url
229
  old_to_new = {
230
+ "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv1/live247-hls-avc/index.m3u8",
231
+ "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv2/live247-hls-avc/index.m3u8",
232
+ "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv3/live247-hls-avc/index.m3u8",
233
+ "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv4/live247-hls-avc/index.m3u8",
234
+ "https://live.fptplay53.net/fnxhd1/vtv5hd_vhls.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv5/live247-hls-avc/index.m3u8",
235
+ "https://live.fptplay53.net/fnxhd1/vtv6hd_vhls.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/index.m3u8",
236
+ "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8": "https://live-a.fptplay53.net/live/media/vtv7/live247-hls-avc/index.m3u8",
237
+ "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/c.hunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv8/live-hls-avc/index.m3u8",
238
+ "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv9/live247-hls-avc/index.m3u8",
239
+ "https://live.fptplay53.net/fnxhd1/vtv10hd_vhls.smil/chunklist.m3u8": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
240
+ "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8": "https://live-a.fptplay53.net/live/media/vtv5/live247-hls-avc/index.m3u8",
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  }
242
  return old_to_new.get(url, url)
243
 
 
255
  if cached is not None:
256
  return cached
257
 
258
+ # PRIMARY: VTV Digital CDN
259
+ if channel_id in VTV_DIGITAL_CDN_URLS:
260
+ url = verify_vtv_digital_url(VTV_DIGITAL_CDN_URLS[channel_id])
261
  if url:
262
+ _set_cache(channel_id, url)
263
+ return url
264
+
265
+ # vtvprime/vtv10: not on VTV Digital CDN, use xemtv.us
266
+ if channel_id in ('vtvprime', 'vtv10'):
267
+ url = fetch_xemtv_us_stream(channel_id) or fetch_xemtivitop_stream(channel_id) or fetch_xemtv_legacy_stream(channel_id)
268
+ if url:
269
+ _set_cache(channel_id, url)
270
+ return url
271
 
272
+ # BACKUP: xemtv.us
273
  url = fetch_xemtv_us_stream(channel_id)
274
  if url:
 
275
  _set_cache(channel_id, url)
276
  return url
277
 
278
+ # FALLBACK: xemtivitop.com
279
  url = fetch_xemtivitop_stream(channel_id)
280
  if url:
 
281
  _set_cache(channel_id, url)
282
  return url
283
 
284
+ # FALLBACK: FPTPlay CDN
285
  url = fetch_fptplay_stream(channel_id)
286
  if url:
287
  _set_cache(channel_id, url)
288
  return url
289
 
290
+ # FALLBACK: VTVGo CDN
291
  url = fetch_vtvgo_stream(channel_id)
292
  if url:
293
  _set_cache(channel_id, url)
294
  return url
295
 
296
+ # LAST RESORT: xemtv.net legacy
297
  url = fetch_xemtv_legacy_stream(channel_id)
298
  if url:
 
299
  _set_cache(channel_id, url)
300
  return url
301
 
 
321
  def proxy_page(url: str = Query(...)):
322
  try:
323
  headers = {**UA}
324
+ if "xemtv.us" in url: headers["Referer"] = "https://xemtv.us/"
325
+ elif "xemtv.net" in url: headers["Referer"] = "https://hd.xemtv.net/"
 
 
326
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
327
+ if r.status_code != 200: return Response(status_code=502, content="upstream error")
 
328
  return Response(content=r.text.encode("utf-8"), media_type="text/html; charset=utf-8", headers={"Access-Control-Allow-Origin": "*"})
329
  except:
330
  return Response(status_code=502, content="proxy error")
 
333
  def proxy_vtv_m3u8(url: str = Query(...)):
334
  try:
335
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
336
+ if "fptplay" in url: headers["Referer"] = "https://fptplay.vn/"; headers["Origin"] = "https://fptplay.vn"
337
+ elif "xemtv" in url: headers["Referer"] = "https://xemtv.us/"
338
+ elif "vtvdigital" in url: headers["Referer"] = "https://vtvgo.vn/"
 
 
 
 
339
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=False)
340
+ if r.status_code != 200: return Response(status_code=502, content="upstream error")
 
341
  content = r.text
342
  lines = content.split('\n')
343
  base_url = url.rsplit('/', 1)[0] + '/' if '/' in url else url
344
  rewritten = []
345
  for line in lines:
346
  line = line.strip()
347
+ if not line or line.startswith('#'): rewritten.append(line)
 
348
  else:
349
+ seg_url = line if line.startswith('http') else base_url + line
 
 
350
  if seg_url.endswith('.m3u8'):
351
  rewritten.append("/api/proxy/m3u8/vtv?url=" + requests.utils.quote(seg_url, safe=""))
352
  else:
 
359
  def proxy_vtv_segment(url: str = Query(...)):
360
  try:
361
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
362
+ if "fptplay" in url: headers["Referer"] = "https://fptplay.vn/"; headers["Origin"] = "https://fptplay.vn"
 
 
363
  r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=False)
364
+ if r.status_code != 200: return Response(status_code=502, content="upstream error")
 
365
  data = r.content
366
+ if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47: data = data[188:]
 
367
  return Response(content=data, media_type="video/mp2t", headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"})
368
  except:
369
+ return Response(status_code=502, content="proxy error")