bep40 commited on
Commit
a5c5040
·
verified ·
1 Parent(s): 9843618

Fix VTV streams: return direct URLs to client, update FPTPlay URLs, add canthotv fallback, add CORS proxy, fix AES-128 key URI rewriting in proxy

Browse files
Files changed (1) hide show
  1. vtv_api.py +132 -167
vtv_api.py CHANGED
@@ -1,11 +1,14 @@
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
  Fetches stream URLs from multiple sources with fallback chain:
4
- 1. VTVGO failover (vtvdigital.vn) - primary, proxied for VPN users
5
- 2. xemtv.net PHP endpoints - fresh signed URLs
6
- 3. FPTPlay CDN - fallback
7
- ALL stream URLs are returned via proxy to avoid geo-blocking on client side.
8
- EPG data scraped from https://vtv.vn/lich-phat-song.htm
 
 
 
9
  """
10
  import re, time, threading
11
  import requests
@@ -24,21 +27,7 @@ router = APIRouter()
24
  UA = {
25
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
26
  "Accept-Language": "vi-VN,vi;q=0.9",
27
- "Referer": "https://hd.xemtv.net/",
28
- }
29
-
30
- XEMTV_PHP_ENDPOINTS = {
31
- "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
32
- "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
33
- "vtv3": "https://hd.xemtv.net/kenh/vtv3.php",
34
- "vtv4": "https://hd.xemtv.net/kenh/vtv4.php",
35
- "vtv5": "https://hd.xemtv.net/kenh/vtv5.php",
36
- "vtv6": "https://hd.xemtv.net/kenh/vtv6.php",
37
- "vtv7": "https://hd.xemtv.net/kenh/vtv7.php",
38
- "vtv8": "https://hd.xemtv.net/kenh/vtv8.php",
39
- "vtv9": "https://hd.xemtv.net/kenh/vtv9.php",
40
- "vtv10": "https://hd.xemtv.net/kenh/vtv10.php",
41
- "vtvprime": "https://hd.xemtv.net/kenh/vtvprime.php",
42
  }
43
 
44
  CHANNEL_NAMES = {
@@ -55,7 +44,21 @@ CHANNEL_NAMES = {
55
  "vtvprime": "VTVPrime",
56
  }
57
 
58
- # VTVGO failover CDN - PRIMARY source for all channels (VTV1-9)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
  VTVGO_FAILOVER = {
60
  "vtv1": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
61
  "vtv2": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
@@ -68,35 +71,30 @@ VTVGO_FAILOVER = {
68
  "vtv9": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv9-manifest.m3u8",
69
  }
70
 
71
- # FPTPlay CDN - fallback source
72
- FPTPLAY_URLS = {
73
- "vtv1": "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
74
- "vtv2": "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
75
- "vtv3": "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
76
- "vtv4": "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
77
- "vtv5": "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8",
78
- "vtv6": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
79
- "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
80
- "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
81
- "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
82
- "vtv10": "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
83
  }
84
 
85
- # Additional fallback URLs for VTV6 and VTV10
86
- VTV6_ALL_SOURCES = [
87
- "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv6-manifest.m3u8",
88
- "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
89
- "https://live.canthotv.vn/live/tv/chunklist.m3u8",
90
- ]
91
-
92
- VTV10_ALL_SOURCES = [
93
- "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
94
- "https://live.canthotv.vn/live/tv/chunklist.m3u8",
95
- ]
 
 
 
96
 
97
  _vtv_cache = {}
98
  _vtv_lock = threading.Lock()
99
- _CACHE_TTL = 180
100
 
101
 
102
  def _cached(key):
@@ -111,15 +109,6 @@ def _set_cache(key, data):
111
  _vtv_cache[key] = {'t': time.time(), 'd': data}
112
 
113
 
114
- def _proxy_url(url):
115
- """Wrap an external URL through our proxy so client browser never connects directly."""
116
- if not url:
117
- return url
118
- if url.startswith('/api/proxy/'):
119
- return url
120
- return '/api/proxy/m3u8/vtv?url=' + quote(url, safe='')
121
-
122
-
123
  def extract_m3u8_from_html(html):
124
  if not html:
125
  return None
@@ -140,37 +129,24 @@ def fetch_xemtv_stream(channel_id):
140
  php_url = XEMTV_PHP_ENDPOINTS.get(channel_id)
141
  if not php_url:
142
  return None
143
- try:
144
- r = requests.get(php_url, headers=UA, timeout=15, allow_redirects=True)
145
- if r.status_code == 200:
146
- m3u8 = extract_m3u8_from_html(r.text)
147
- if m3u8:
148
- return m3u8
149
- except:
150
- pass
151
- return None
152
-
153
-
154
- def fetch_fptplay_stream(channel_id):
155
- url = FPTPLAY_URLS.get(channel_id)
156
- if not url:
157
- return None
158
  try:
159
  headers = {
160
  "User-Agent": UA["User-Agent"],
161
- "Referer": "https://fptplay.vn/",
162
- "Origin": "https://fptplay.vn"
 
163
  }
164
- r = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
165
- if r.status_code == 200:
166
- return url
 
 
167
  except:
168
  pass
169
  return None
170
 
171
 
172
  def _verify_stream_url(url, timeout=8):
173
- """Quick check if a stream URL is actually serving content."""
174
  if not url:
175
  return False
176
  try:
@@ -179,33 +155,38 @@ def _verify_stream_url(url, timeout=8):
179
  headers["Referer"] = "https://vtv.vn/"
180
  elif "fptplay" in url:
181
  headers["Referer"] = "https://fptplay.vn/"
 
 
 
182
  verify_ssl = "vtvdigital" not in url
183
  r = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True, verify=verify_ssl)
184
- if r.status_code == 200 and b"#EXTM3U" in r.content[:200]:
185
- base = url.rsplit('/', 1)[0] + '/'
186
  lines = r.text.strip().split('\n')
187
- for line in lines:
188
- line = line.strip() if isinstance(line, str) else line.strip()
189
- if line and not line.startswith('#'):
190
- sub_url = line if line.startswith('http') else base + line
191
- try:
192
- sub_r = requests.get(sub_url, headers=headers, timeout=timeout,
193
- allow_redirects=True, verify=verify_ssl)
194
- if sub_r.status_code == 200 and b"#EXTM3U" in sub_r.content[:200]:
195
- return True
196
- except:
197
- pass
198
- break
 
 
 
 
 
 
 
199
  return False
200
  except:
201
  return False
202
 
203
 
204
  def fetch_vtv_stream(channel_id):
205
- """
206
- Fetch VTV stream URL with multi-source fallback chain.
207
- Returns a PROXIED URL so client browser never connects directly.
208
- """
209
  channel_id = channel_id.lower().strip()
210
  name_map = {
211
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
@@ -218,78 +199,36 @@ def fetch_vtv_stream(channel_id):
218
  cached = _cached(channel_id)
219
  if cached is not None:
220
  return cached
221
-
222
  result = None
223
-
224
- # VTV6 special: try all sources
225
- if channel_id == 'vtv6':
226
- xemtv_url = fetch_xemtv_stream('vtv6')
227
- if xemtv_url:
228
- result = _proxy_url(xemtv_url)
229
- _set_cache(channel_id, result)
230
- return result
231
- for src in VTV6_ALL_SOURCES:
232
- if _verify_stream_url(src):
233
- result = _proxy_url(src)
234
- _set_cache(channel_id, result)
235
- return result
236
- result = _proxy_url(VTV6_ALL_SOURCES[0])
237
- _set_cache(channel_id, result)
238
- return result
239
-
240
- # VTV10 special: no VTVGO failover
241
- if channel_id == 'vtv10':
242
- xemtv_url = fetch_xemtv_stream('vtv10')
243
- if xemtv_url:
244
- result = _proxy_url(xemtv_url)
245
- _set_cache(channel_id, result)
246
- return result
247
- for src in VTV10_ALL_SOURCES:
248
- if _verify_stream_url(src):
249
- result = _proxy_url(src)
250
- _set_cache(channel_id, result)
251
- return result
252
- result = _proxy_url(VTV10_ALL_SOURCES[0])
253
- _set_cache(channel_id, result)
254
- return result
255
-
256
- # VTVPrime
257
  if channel_id == 'vtvprime':
258
  xemtv_url = fetch_xemtv_stream('vtvprime')
259
  if xemtv_url:
260
- result = _proxy_url(xemtv_url)
261
  _set_cache(channel_id, result)
262
  return result
263
  _set_cache(channel_id, None)
264
  return None
265
-
266
- # VTV1-VTV9: VTVGO failover primary, xemtv secondary
267
- if channel_id in VTVGO_FAILOVER:
268
- failover_url = VTVGO_FAILOVER[channel_id]
269
- if _verify_stream_url(failover_url):
270
- result = _proxy_url(failover_url)
271
- _set_cache(channel_id, result)
272
- return result
273
- result = _proxy_url(failover_url)
274
- _set_cache(channel_id, result)
275
- return result
276
-
277
- # Fallback: xemtv
278
  xemtv_url = fetch_xemtv_stream(channel_id)
279
  if xemtv_url:
280
- result = _proxy_url(xemtv_url)
281
- _set_cache(channel_id, result)
282
- return result
283
-
284
- # Fallback: FPTPlay
285
- fpt_url = fetch_fptplay_stream(channel_id)
286
- if fpt_url:
287
- result = _proxy_url(fpt_url)
288
- _set_cache(channel_id, result)
289
- return result
290
-
291
- _set_cache(channel_id, None)
292
- return None
293
 
294
 
295
  @router.get("/api/vtv/streams")
@@ -330,7 +269,6 @@ def proxy_page(url: str = Query(...)):
330
 
331
  @router.get("/api/proxy/m3u8/vtv")
332
  def proxy_vtv_m3u8(url: str = Query(...)):
333
- """Proxy m3u8 manifest + rewrite ALL sub-manifest/segment URLs to go through proxy."""
334
  try:
335
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
336
  if "fptplay" in url:
@@ -343,23 +281,28 @@ def proxy_vtv_m3u8(url: str = Query(...)):
343
  headers["Origin"] = "https://vtv.vn"
344
  elif "canthotv" in url:
345
  headers["Referer"] = "https://canthotv.vn/"
346
-
347
  verify_ssl = "vtvdigital" not in url
348
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=verify_ssl)
349
  if r.status_code != 200:
350
  return Response(status_code=502, content="upstream error")
351
-
352
  content = r.text
353
  lines = content.split('\n')
354
  rewritten = []
355
  base_url = url.rsplit('/', 1)[0] + '/'
356
-
357
  for line in lines:
358
  line = line.strip()
359
  if not line:
360
  rewritten.append(line)
361
  continue
362
  if line.startswith('#'):
 
 
 
 
 
 
 
 
363
  rewritten.append(line)
364
  continue
365
  seg_url = line
@@ -367,7 +310,6 @@ def proxy_vtv_m3u8(url: str = Query(...)):
367
  seg_url = base_url + seg_url
368
  proxy_seg = "/api/proxy/seg/vtv?url=" + quote(seg_url, safe="")
369
  rewritten.append(proxy_seg)
370
-
371
  return Response(
372
  content='\n'.join(rewritten).encode("utf-8"),
373
  media_type="application/vnd.apple.mpegurl",
@@ -379,7 +321,6 @@ def proxy_vtv_m3u8(url: str = Query(...)):
379
 
380
  @router.get("/api/proxy/seg/vtv")
381
  def proxy_vtv_segment(url: str = Query(...)):
382
- """Proxy individual segments. Also handles sub-manifests (nested m3u8)."""
383
  try:
384
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
385
  if "fptplay" in url:
@@ -390,14 +331,11 @@ def proxy_vtv_segment(url: str = Query(...)):
390
  headers["Origin"] = "https://vtv.vn"
391
  elif "canthotv" in url:
392
  headers["Referer"] = "https://canthotv.vn/"
393
-
394
  verify_ssl = "vtvdigital" not in url
395
  r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=verify_ssl)
396
  if r.status_code != 200:
397
  return Response(status_code=502, content="upstream error")
398
-
399
  data = r.content
400
-
401
  if b"#EXTM3U" in data[:500] or ".m3u8" in url:
402
  lines = data.decode("utf-8", errors="replace").split('\n')
403
  rewritten = []
@@ -405,6 +343,14 @@ def proxy_vtv_segment(url: str = Query(...)):
405
  for line in lines:
406
  line_s = line.strip()
407
  if not line_s or line_s.startswith('#'):
 
 
 
 
 
 
 
 
408
  rewritten.append(line_s)
409
  else:
410
  seg_url = line_s
@@ -416,10 +362,8 @@ def proxy_vtv_segment(url: str = Query(...)):
416
  media_type="application/vnd.apple.mpegurl",
417
  headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache"}
418
  )
419
-
420
  if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
421
  data = data[188:]
422
-
423
  return Response(
424
  content=data,
425
  media_type="video/mp2t",
@@ -429,7 +373,28 @@ def proxy_vtv_segment(url: str = Query(...)):
429
  return Response(status_code=502, content="proxy error")
430
 
431
 
432
- # EPG
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
433
  _epg_cache = {}
434
  _epg_cache_time = 0
435
  _EPG_CACHE_TTL = 1800
 
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
  Fetches stream URLs from multiple sources with fallback chain:
4
+ 1. FPTPlay CDN - primary (direct URLs, client fetches directly)
5
+ 2. VTVGO failover (vtvdigital.vn) - secondary
6
+ 3. canthotv.vn - fallback for VTV6/VTV10
7
+ 4. xemtv.net PHP endpoints - last resort (often blocked by Cloudflare)
8
+
9
+ KEY CHANGE: Stream URLs are returned DIRECTLY to client (not proxied through server).
10
+ This avoids server-side 403/404 issues since the client browser (with VPN) fetches directly.
11
+ CORS is handled by the client-side player using hls.js with corsConfig.
12
  """
13
  import re, time, threading
14
  import requests
 
27
  UA = {
28
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36",
29
  "Accept-Language": "vi-VN,vi;q=0.9",
30
+ "Referer": "https://fptplay.vn/",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
31
  }
32
 
33
  CHANNEL_NAMES = {
 
44
  "vtvprime": "VTVPrime",
45
  }
46
 
47
+ # ===== FPTPlay CDN - PRIMARY source =====
48
+ FPTPLAY_URLS = {
49
+ "vtv1": "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
50
+ "vtv2": "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
51
+ "vtv3": "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
52
+ "vtv4": "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
53
+ "vtv5": "https://live.fptplay53.net/fnxhd1/vtv5hd_vhls.smil/chunklist.m3u8",
54
+ "vtv6": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
55
+ "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist.m3u8",
56
+ "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
57
+ "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
58
+ "vtv10": "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
59
+ }
60
+
61
+ # ===== VTVGO failover CDN - SECONDARY source =====
62
  VTVGO_FAILOVER = {
63
  "vtv1": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
64
  "vtv2": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
 
71
  "vtv9": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv9-manifest.m3u8",
72
  }
73
 
74
+ # ===== canthotv.vn - FALLBACK =====
75
+ CANTHOTV_URLS = {
76
+ "vtv6": "https://live.canthotv.vn/live/tv/chunklist.m3u8",
77
+ "vtv10": "https://live.canthotv.vn/live/tv/chunklist.m3u8",
 
 
 
 
 
 
 
 
78
  }
79
 
80
+ # ===== xemtv.net PHP endpoints - LAST RESORT =====
81
+ XEMTV_PHP_ENDPOINTS = {
82
+ "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
83
+ "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
84
+ "vtv3": "https://hd.xemtv.net/kenh/vtv3.php",
85
+ "vtv4": "https://hd.xemtv.net/kenh/vtv4.php",
86
+ "vtv5": "https://hd.xemtv.net/kenh/vtv5.php",
87
+ "vtv6": "https://hd.xemtv.net/kenh/vtv6.php",
88
+ "vtv7": "https://hd.xemtv.net/kenh/vtv7.php",
89
+ "vtv8": "https://hd.xemtv.net/kenh/vtv8.php",
90
+ "vtv9": "https://hd.xemtv.net/kenh/vtv9.php",
91
+ "vtv10": "https://hd.xemtv.net/kenh/vtv10.php",
92
+ "vtvprime": "https://hd.xemtv.net/kenh/vtvprime.php",
93
+ }
94
 
95
  _vtv_cache = {}
96
  _vtv_lock = threading.Lock()
97
+ _CACHE_TTL = 120
98
 
99
 
100
  def _cached(key):
 
109
  _vtv_cache[key] = {'t': time.time(), 'd': data}
110
 
111
 
 
 
 
 
 
 
 
 
 
112
  def extract_m3u8_from_html(html):
113
  if not html:
114
  return None
 
129
  php_url = XEMTV_PHP_ENDPOINTS.get(channel_id)
130
  if not php_url:
131
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
132
  try:
133
  headers = {
134
  "User-Agent": UA["User-Agent"],
135
+ "Accept-Language": "vi-VN,vi;q=0.9",
136
+ "Referer": "https://hd.xemtv.net/",
137
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
138
  }
139
+ r = requests.get(php_url, headers=headers, timeout=15, allow_redirects=True)
140
+ if r.status_code == 200 and "cloudflare" not in r.text.lower() and "just a moment" not in r.text.lower():
141
+ m3u8 = extract_m3u8_from_html(r.text)
142
+ if m3u8:
143
+ return m3u8
144
  except:
145
  pass
146
  return None
147
 
148
 
149
  def _verify_stream_url(url, timeout=8):
 
150
  if not url:
151
  return False
152
  try:
 
155
  headers["Referer"] = "https://vtv.vn/"
156
  elif "fptplay" in url:
157
  headers["Referer"] = "https://fptplay.vn/"
158
+ headers["Origin"] = "https://fptplay.vn"
159
+ elif "canthotv" in url:
160
+ headers["Referer"] = "https://canthotv.vn/"
161
  verify_ssl = "vtvdigital" not in url
162
  r = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True, verify=verify_ssl)
163
+ if r.status_code == 200 and b"#EXTM3U" in r.content[:500]:
 
164
  lines = r.text.strip().split('\n')
165
+ has_ts = any('.ts' in line.lower() for line in lines if not line.startswith('#'))
166
+ has_sub = any('.m3u8' in line.lower() for line in lines if not line.startswith('#'))
167
+ if has_ts:
168
+ return True
169
+ if has_sub:
170
+ for line in lines:
171
+ line = line.strip()
172
+ if line and not line.startswith('#') and '.m3u8' in line:
173
+ sub_url = line if line.startswith('http') else url.rsplit('/', 1)[0] + '/' + line
174
+ try:
175
+ sub_r = requests.get(sub_url, headers=headers, timeout=timeout, allow_redirects=True, verify=verify_ssl)
176
+ if sub_r.status_code == 200 and b"#EXTM3U" in sub_r.content[:500]:
177
+ sub_lines = sub_r.text.strip().split('\n')
178
+ has_real_ts = any('.ts' in l.lower() for l in sub_lines if not l.startswith('#'))
179
+ if has_real_ts:
180
+ return True
181
+ except:
182
+ pass
183
+ break
184
  return False
185
  except:
186
  return False
187
 
188
 
189
  def fetch_vtv_stream(channel_id):
 
 
 
 
190
  channel_id = channel_id.lower().strip()
191
  name_map = {
192
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
 
199
  cached = _cached(channel_id)
200
  if cached is not None:
201
  return cached
 
202
  result = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  if channel_id == 'vtvprime':
204
  xemtv_url = fetch_xemtv_stream('vtvprime')
205
  if xemtv_url:
206
+ result = xemtv_url
207
  _set_cache(channel_id, result)
208
  return result
209
  _set_cache(channel_id, None)
210
  return None
211
+ sources = []
212
+ fpt_url = FPTPLAY_URLS.get(channel_id)
213
+ if fpt_url:
214
+ sources.append(fpt_url)
215
+ vtvgo_url = VTVGO_FAILOVER.get(channel_id)
216
+ if vtvgo_url:
217
+ sources.append(vtvgo_url)
218
+ cantho_url = CANTHOTV_URLS.get(channel_id)
219
+ if cantho_url:
220
+ sources.append(cantho_url)
 
 
 
221
  xemtv_url = fetch_xemtv_stream(channel_id)
222
  if xemtv_url:
223
+ sources.append(xemtv_url)
224
+ for src in sources:
225
+ if _verify_stream_url(src):
226
+ result = src
227
+ break
228
+ if result is None and sources:
229
+ result = sources[0]
230
+ _set_cache(channel_id, result)
231
+ return result
 
 
 
 
232
 
233
 
234
  @router.get("/api/vtv/streams")
 
269
 
270
  @router.get("/api/proxy/m3u8/vtv")
271
  def proxy_vtv_m3u8(url: str = Query(...)):
 
272
  try:
273
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
274
  if "fptplay" in url:
 
281
  headers["Origin"] = "https://vtv.vn"
282
  elif "canthotv" in url:
283
  headers["Referer"] = "https://canthotv.vn/"
 
284
  verify_ssl = "vtvdigital" not in url
285
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True, verify=verify_ssl)
286
  if r.status_code != 200:
287
  return Response(status_code=502, content="upstream error")
 
288
  content = r.text
289
  lines = content.split('\n')
290
  rewritten = []
291
  base_url = url.rsplit('/', 1)[0] + '/'
 
292
  for line in lines:
293
  line = line.strip()
294
  if not line:
295
  rewritten.append(line)
296
  continue
297
  if line.startswith('#'):
298
+ if 'URI=' in line:
299
+ line = re.sub(
300
+ r'URI="([^"]+)"',
301
+ lambda m: 'URI="' + ("/api/proxy/seg/vtv?url=" + quote(m.group(1), safe="")
302
+ if not m.group(1).startswith('http')
303
+ else "/api/proxy/seg/vtv?url=" + quote(m.group(1), safe="")) + '"',
304
+ line
305
+ )
306
  rewritten.append(line)
307
  continue
308
  seg_url = line
 
310
  seg_url = base_url + seg_url
311
  proxy_seg = "/api/proxy/seg/vtv?url=" + quote(seg_url, safe="")
312
  rewritten.append(proxy_seg)
 
313
  return Response(
314
  content='\n'.join(rewritten).encode("utf-8"),
315
  media_type="application/vnd.apple.mpegurl",
 
321
 
322
  @router.get("/api/proxy/seg/vtv")
323
  def proxy_vtv_segment(url: str = Query(...)):
 
324
  try:
325
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
326
  if "fptplay" in url:
 
331
  headers["Origin"] = "https://vtv.vn"
332
  elif "canthotv" in url:
333
  headers["Referer"] = "https://canthotv.vn/"
 
334
  verify_ssl = "vtvdigital" not in url
335
  r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=verify_ssl)
336
  if r.status_code != 200:
337
  return Response(status_code=502, content="upstream error")
 
338
  data = r.content
 
339
  if b"#EXTM3U" in data[:500] or ".m3u8" in url:
340
  lines = data.decode("utf-8", errors="replace").split('\n')
341
  rewritten = []
 
343
  for line in lines:
344
  line_s = line.strip()
345
  if not line_s or line_s.startswith('#'):
346
+ if 'URI=' in line_s:
347
+ line_s = re.sub(
348
+ r'URI="([^"]+)"',
349
+ lambda m: 'URI="' + ("/api/proxy/seg/vtv?url=" + quote(m.group(1), safe="")
350
+ if not m.group(1).startswith('http')
351
+ else "/api/proxy/seg/vtv?url=" + quote(m.group(1), safe="")) + '"',
352
+ line_s
353
+ )
354
  rewritten.append(line_s)
355
  else:
356
  seg_url = line_s
 
362
  media_type="application/vnd.apple.mpegurl",
363
  headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "no-cache"}
364
  )
 
365
  if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
366
  data = data[188:]
 
367
  return Response(
368
  content=data,
369
  media_type="video/mp2t",
 
373
  return Response(status_code=502, content="proxy error")
374
 
375
 
376
+ @router.get("/api/cors-proxy")
377
+ def cors_proxy(url: str = Query(...)):
378
+ try:
379
+ headers = {"User-Agent": UA["User-Agent"]}
380
+ if "fptplay" in url:
381
+ headers["Referer"] = "https://fptplay.vn/"
382
+ headers["Origin"] = "https://fptplay.vn"
383
+ elif "vtvdigital" in url:
384
+ headers["Referer"] = "https://vtv.vn/"
385
+ elif "canthotv" in url:
386
+ headers["Referer"] = "https://canthotv.vn/"
387
+ verify_ssl = "vtvdigital" not in url
388
+ r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, verify=verify_ssl)
389
+ resp_headers = {"Access-Control-Allow-Origin": "*"}
390
+ if "Content-Type" in r.headers:
391
+ resp_headers["Content-Type"] = r.headers["Content-Type"]
392
+ return Response(content=r.content, status_code=r.status_code, headers=resp_headers)
393
+ except Exception as e:
394
+ return Response(status_code=502, content=str(e))
395
+
396
+
397
+ # ===== EPG =====
398
  _epg_cache = {}
399
  _epg_cache_time = 0
400
  _EPG_CACHE_TTL = 1800