bep40 commited on
Commit
036e9d9
·
verified ·
1 Parent(s): 46881f5

vtv_api v2 — multi-source auto-failover

Browse files
Files changed (1) hide show
  1. vtv_api.py +151 -167
vtv_api.py CHANGED
@@ -1,6 +1,11 @@
1
  """
2
- VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
- Fetches stream URLs from hd.xemtv.net PHP endpoints and VTVPrime
 
 
 
 
 
4
  """
5
  import re, time, threading
6
  import requests
@@ -15,69 +20,81 @@ UA = {
15
  "Referer": "https://hd.xemtv.net/",
16
  }
17
 
18
- # Channel ID -> xemtv.net PHP endpoint mapping
19
- XEMTV_PHP_ENDPOINTS = {
20
- "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
21
- "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
22
- "vtv3": "https://hd.xemtv.net/kenh/vtv3.php",
23
- "vtv4": "https://hd.xemtv.net/kenh/vtv4.php",
24
- "vtv5": "https://hd.xemtv.net/kenh/vtv5.php",
25
- "vtv6": "https://hd.xemtv.net/kenh/vtv6.php",
26
- "vtv7": "https://hd.xemtv.net/kenh/vtv7.php",
27
- "vtv8": "https://hd.xemtv.net/kenh/vtv8.php",
28
- "vtv9": "https://hd.xemtv.net/kenh/vtv9.php",
29
- "vtv10": "https://hd.xemtv.net/kenh/vtv10.php",
30
- "vtvprime": "https://hd.xemtv.net/kenh/vtvprime.php",
31
- }
32
-
33
- # Channel display names
34
  CHANNEL_NAMES = {
35
- "vtv1": "VTV1",
36
- "vtv2": "VTV2",
37
- "vtv3": "VTV3",
38
- "vtv4": "VTV4",
39
- "vtv5": "VTV5",
40
- "vtv6": "VTV6",
41
- "vtv7": "VTV7",
42
- "vtv8": "VTV8",
43
- "vtv9": "VTV9",
44
- "vtv10": "VTV10",
45
- "vtvprime": "VTVPrime",
46
  }
47
 
48
- # ===== PRIMARY: VTVGo failover CDN (verified working 2026) =====
49
- VTVGO_FAILOVER = {
50
- "vtv1": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
51
- "vtv2": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
52
- "vtv3": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv3-manifest.m3u8",
53
- "vtv4": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv4-manifest.m3u8",
54
- "vtv5": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv5-manifest.m3u8",
55
- "vtv7": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv7-manifest.m3u8",
56
- "vtv8": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv8-manifest.m3u8",
57
- "vtv9": "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv9-manifest.m3u8",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
58
  }
59
 
60
- # ===== Channels that should use xemtv.net scraping (not VTVGo failover) =====
61
- # VTV6, VTV10: xemtv.net scraping (returns fptplay CDN URLs that need proxying)
62
- XEMTV_ONLY_CHANNELS = {"vtv6", "vtv10"}
63
-
64
- # ===== LAST RESORT: fptplay CDN URLs (need proxy for referer) =====
65
- FPTPLAY_URLS = {
66
- "vtv1": "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
67
- "vtv2": "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
68
- "vtv3": "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
69
- "vtv4": "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
70
- "vtv5": "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8",
71
- "vtv6": "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
72
- "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
73
- "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
74
- "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
75
- "vtv10": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/vtv10-avc1_5600000=10000-mp4a_131600=20000.m3u8",
76
  }
77
 
78
  _vtv_cache = {}
79
  _vtv_lock = threading.Lock()
80
- _CACHE_TTL = 180
81
 
82
 
83
  def _cached(key):
@@ -92,16 +109,42 @@ def _set_cache(key, data):
92
  _vtv_cache[key] = {'t': time.time(), 'd': data}
93
 
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def extract_m3u8_from_html(html):
96
- """Extract m3u8 URL from xemtv PHP page (jwplayer config)."""
97
  if not html:
98
  return None
99
- m = re.search(r"file\s*:\s*['\"]([^'\"]*\.m3u8[^'\"]*)['\"]", html, re.IGNORECASE)
 
100
  if m:
101
  url = m.group(1).strip()
102
  if len(url) > 20:
103
  return url
 
104
  m = re.search(r"(https?://[^\s\"'<>\\]+\.m3u8[^\s\"'<>\\]*)", html, re.IGNORECASE)
 
 
 
 
 
 
105
  if m:
106
  url = m.group(1).strip()
107
  if len(url) > 20:
@@ -111,34 +154,15 @@ def extract_m3u8_from_html(html):
111
 
112
  def fetch_xemtv_stream(channel_id):
113
  """Scrape xemtv.net PHP page for fresh stream URL."""
114
- php_url = XEMTV_PHP_ENDPOINTS.get(channel_id)
115
  if not php_url:
116
  return None
117
  try:
118
- r = requests.get(php_url, headers=UA, timeout=15, allow_redirects=True)
119
  if r.status_code == 200:
120
- m3u8 = extract_m3u8_from_html(r.text)
121
- if m3u8:
122
- return m3u8
123
- except:
124
- pass
125
- return None
126
-
127
-
128
- def fetch_fptplay_stream(channel_id):
129
- """Fetch fptplay CDN stream URL with proper Referer header."""
130
- url = FPTPLAY_URLS.get(channel_id)
131
- if not url:
132
- return None
133
- try:
134
- headers = {
135
- "User-Agent": UA["User-Agent"],
136
- "Referer": "https://fptplay.vn/",
137
- "Origin": "https://fptplay.vn",
138
- }
139
- r = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
140
- if r.status_code == 200:
141
- return url
142
  except:
143
  pass
144
  return None
@@ -146,19 +170,13 @@ def fetch_fptplay_stream(channel_id):
146
 
147
  def fetch_vtv_stream(channel_id):
148
  """
149
- Fetch m3u8 stream URL for a VTV channel.
150
- Priority:
151
- 1. VTV1-VTV5, VTV7-VTV9: VTVGo failover CDN (no referer needed)
152
- 2. VTV6, VTV10: xemtv.net scraping (user requirement) -> fptplay URL (proxied)
153
- 3. VTVPrime: try xemtv, else None (paid channel)
154
  """
155
  channel_id = channel_id.lower().strip()
156
  name_map = {
157
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
158
  'vtv_can_tho': 'vtv10', 'cantho': 'vtv10',
159
- 'vietnam_vtv1': 'vtv1', 'vietnam_vtv2': 'vtv2', 'vietnam_vtv3': 'vtv3',
160
- 'vietnam_vtv4': 'vtv4', 'vietnam_vtv5': 'vtv5', 'vietnam_vtv6': 'vtv6',
161
- 'vietnam_vtv7': 'vtv7', 'vietnam_vtv8': 'vtv8', 'vietnam_vtv9': 'vtv9',
162
  }
163
  channel_id = name_map.get(channel_id, channel_id)
164
 
@@ -166,69 +184,42 @@ def fetch_vtv_stream(channel_id):
166
  if cached is not None:
167
  return cached
168
 
169
- # --- VTV6: xemtv.net scraping (returns working fptplay URL) ---
170
- # --- VTV10: xemtv returns vtvcantho (403), skip to fptplay CDN directly ---
171
- if channel_id in XEMTV_ONLY_CHANNELS:
172
- if channel_id == "vtv10":
173
- # VTV10: xemtv.net returns dead vtvcantho URL (403), use fptplay CDN directly
174
- fpt_url = FPTPLAY_URLS.get(channel_id)
175
- if fpt_url:
176
- _set_cache(channel_id, fpt_url)
177
- return fpt_url
178
- _set_cache(channel_id, None)
179
- return None
180
- xemtv_url = fetch_xemtv_stream(channel_id)
181
- if xemtv_url:
182
- _set_cache(channel_id, xemtv_url)
183
- return xemtv_url
184
- fpt_url = FPTPLAY_URLS.get(channel_id)
185
- if fpt_url:
186
- _set_cache(channel_id, fpt_url)
187
- return fpt_url
188
- _set_cache(channel_id, None)
189
- return None
190
-
191
- # --- VTV1-VTV5, VTV7-VTV9: VTVGo failover ---
192
- if channel_id in VTVGO_FAILOVER:
193
- result = VTVGO_FAILOVER[channel_id]
194
- _set_cache(channel_id, result)
195
- return result
196
-
197
- # --- VTVPrime ---
198
- if channel_id == 'vtvprime':
199
- xemtv_url = fetch_xemtv_stream('vtvprime')
200
- if xemtv_url:
201
- _set_cache(channel_id, xemtv_url)
202
- return xemtv_url
203
- _set_cache(channel_id, None)
204
- return None
205
 
206
- # --- Fallback: xemtv scraping ---
207
  xemtv_url = fetch_xemtv_stream(channel_id)
208
  if xemtv_url:
209
- _set_cache(channel_id, xemtv_url)
210
- return xemtv_url
 
211
 
212
- # --- Last resort: fptplay ---
213
- fpt_url = fetch_fptplay_stream(channel_id)
214
- if fpt_url:
215
- _set_cache(channel_id, fpt_url)
216
- return fpt_url
 
 
 
217
 
218
- _set_cache(channel_id, None)
219
- return None
 
 
220
 
221
 
222
  @router.get("/api/vtv/streams")
223
  def api_vtv_streams():
224
- """Get all VTV channel stream URLs."""
225
  result = {}
226
  for ch_id in CHANNEL_NAMES:
227
- stream_url = fetch_vtv_stream(ch_id)
228
  result[ch_id] = {
229
  "name": CHANNEL_NAMES[ch_id],
230
- "stream_url": stream_url,
231
- "status": "ok" if stream_url else "offline",
 
 
232
  }
233
  return JSONResponse(result)
234
 
@@ -236,48 +227,34 @@ def api_vtv_streams():
236
  @router.get("/api/vtv/stream/{channel_id}")
237
  def api_vtv_stream(channel_id: str):
238
  """Get stream URL for a specific VTV channel."""
239
- stream_url = fetch_vtv_stream(channel_id)
240
- if stream_url:
241
- return JSONResponse({"stream_url": stream_url, "status": "ok"})
242
  return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
243
 
244
 
245
- @router.get("/api/proxy/page")
246
- def proxy_page(url: str = Query(...)):
247
- """Proxy a web page."""
248
- try:
249
- headers = {**UA}
250
- if "xemtv.net" in url:
251
- headers["Referer"] = "https://hd.xemtv.net/"
252
- r = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
253
- if r.status_code != 200:
254
- return Response(status_code=502, content="upstream error")
255
- return Response(
256
- content=r.text.encode("utf-8"),
257
- media_type="text/html; charset=utf-8",
258
- headers={"Access-Control-Allow-Origin": "*"}
259
- )
260
- except:
261
- return Response(status_code=502, content="proxy error")
262
-
263
-
264
  @router.get("/api/proxy/m3u8/vtv")
265
  def proxy_vtv_m3u8(url: str = Query(...)):
266
- """Proxy an m3u8 stream URL with proper headers for fptplay CDN."""
267
  try:
268
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
269
  if "fptplay" in url:
270
  headers["Referer"] = "https://fptplay.vn/"
271
  headers["Origin"] = "https://fptplay.vn"
 
 
272
  elif "xemtv" in url:
273
  headers["Referer"] = "https://hd.xemtv.net/"
 
274
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
275
  if r.status_code != 200:
276
- return Response(status_code=502, content="upstream error")
 
277
  content = r.text
278
  lines = content.split('\n')
279
  rewritten = []
280
  base_url = url.rsplit('/', 1)[0] + '/'
 
281
  for line in lines:
282
  line = line.strip()
283
  if not line or line.startswith('#'):
@@ -287,6 +264,7 @@ def proxy_vtv_m3u8(url: str = Query(...)):
287
  if not seg_url.startswith('http'):
288
  seg_url = base_url + seg_url
289
  rewritten.append("/api/proxy/seg/vtv?url=" + requests.utils.quote(seg_url, safe=""))
 
290
  return Response(
291
  content='\n'.join(rewritten).encode('utf-8'),
292
  media_type="application/vnd.apple.mpegurl",
@@ -298,18 +276,24 @@ def proxy_vtv_m3u8(url: str = Query(...)):
298
 
299
  @router.get("/api/proxy/seg/vtv")
300
  def proxy_vtv_segment(url: str = Query(...)):
301
- """Proxy a video segment with proper headers for fptplay CDN."""
302
  try:
303
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
304
  if "fptplay" in url:
305
  headers["Referer"] = "https://fptplay.vn/"
306
  headers["Origin"] = "https://fptplay.vn"
307
- r = requests.get(url, headers=headers, timeout=30, allow_redirects=True)
 
 
 
308
  if r.status_code != 200:
309
- return Response(status_code=502, content="upstream error")
 
310
  data = r.content
 
311
  if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
312
  data = data[188:]
 
313
  return Response(
314
  content=data,
315
  media_type="video/mp2t",
 
1
  """
2
+ VTV Channels API v2 Multi-source with auto-failover
3
+ Sources per channel (priority order):
4
+ 1. xemtv.net scraping (fresh, changes dynamically)
5
+ 2. VTVGo failover CDN (vtvgolive-failover.vtvdigital.vn)
6
+ 3. fptplay CDN (live247 / fnxch2 / epzhd1)
7
+ 4. fptplay ABR (.smil/chunklist)
8
+ Every source is verified with HTTP HEAD before returning.
9
  """
10
  import re, time, threading
11
  import requests
 
20
  "Referer": "https://hd.xemtv.net/",
21
  }
22
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
  CHANNEL_NAMES = {
24
+ "vtv1": "VTV1", "vtv2": "VTV2", "vtv3": "VTV3", "vtv4": "VTV4",
25
+ "vtv5": "VTV5", "vtv6": "VTV6", "vtv7": "VTV7", "vtv8": "VTV8",
26
+ "vtv9": "VTV9", "vtv10": "VTV10", "vtvprime": "VTVPrime",
 
 
 
 
 
 
 
 
27
  }
28
 
29
+ # ===== SOURCE LISTS multiple backups per channel =====
30
+ # Each channel has a list of URLs to try in order
31
+ CHANNEL_SOURCES = {
32
+ "vtv1": [
33
+ "https://live-a.fptplay53.net/live/media/vtv1/live247-hls-avc/index.m3u8",
34
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv1-manifest.m3u8",
35
+ "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
36
+ ],
37
+ "vtv2": [
38
+ "https://live-a.fptplay53.net/live/media/vtv2/live247-hls-avc/index.m3u8",
39
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv2-manifest.m3u8",
40
+ "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
41
+ ],
42
+ "vtv3": [
43
+ "https://live-a.fptplay53.net/live/media/vtv3/live247-hls-avc/index.m3u8",
44
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv3-manifest.m3u8",
45
+ "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
46
+ ],
47
+ "vtv4": [
48
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv4-manifest.m3u8",
49
+ "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
50
+ ],
51
+ "vtv5": [
52
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv5-manifest.m3u8",
53
+ "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8",
54
+ ],
55
+ "vtv6": [
56
+ "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/vtv6-avc1_5600000=10000-mp4a_131600=20000.m3u8",
57
+ "https://live-a.fptplay53.net/live/media/vtv6/live247-hls-avc/index.m3u8",
58
+ ],
59
+ "vtv7": [
60
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv7-manifest.m3u8",
61
+ "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
62
+ ],
63
+ "vtv8": [
64
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv8-manifest.m3u8",
65
+ "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
66
+ ],
67
+ "vtv9": [
68
+ "https://vtvgolive-failover.vtvdigital.vn/vtvgo/vtv9-manifest.m3u8",
69
+ "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
70
+ ],
71
+ "vtv10": [
72
+ "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/vtv10-avc1_5600000=10000-mp4a_131600=20000.m3u8",
73
+ "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/index.m3u8",
74
+ ],
75
+ "vtvprime": [
76
+ "https://live.fptplay53.net/fnxch2/vtvprime_abr.smil/chunklist.m3u8",
77
+ ],
78
  }
79
 
80
+ # xemtv.net PHP endpoints for dynamic scraping
81
+ XEMTV_PHP = {
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 # 2 min cache
98
 
99
 
100
  def _cached(key):
 
109
  _vtv_cache[key] = {'t': time.time(), 'd': data}
110
 
111
 
112
+ def _verify_url(url, timeout=5):
113
+ """Verify a stream URL is reachable with HEAD request."""
114
+ try:
115
+ headers = {"User-Agent": UA["User-Agent"]}
116
+ if "fptplay" in url:
117
+ headers["Referer"] = "https://fptplay.vn/"
118
+ headers["Origin"] = "https://fptplay.vn"
119
+ r = requests.head(url, headers=headers, timeout=timeout, allow_redirects=True)
120
+ if r.status_code == 200:
121
+ return True
122
+ # Some servers don't support HEAD, try GET with range
123
+ headers["Range"] = "bytes=0-0"
124
+ r = requests.get(url, headers=headers, timeout=timeout, allow_redirects=True)
125
+ return r.status_code in (200, 206)
126
+ except:
127
+ return False
128
+
129
+
130
  def extract_m3u8_from_html(html):
131
+ """Extract m3u8/smil URL from xemtv PHP page."""
132
  if not html:
133
  return None
134
+ # jwplayer file: 'URL'
135
+ m = re.search(r"file\s*:\s*['\"]([^'\"]+\.(?:m3u8|smil)[^'\"]*)['\"]", html, re.IGNORECASE)
136
  if m:
137
  url = m.group(1).strip()
138
  if len(url) > 20:
139
  return url
140
+ # Generic m3u8 URL
141
  m = re.search(r"(https?://[^\s\"'<>\\]+\.m3u8[^\s\"'<>\\]*)", html, re.IGNORECASE)
142
+ if m:
143
+ url = m.group(1).strip()
144
+ if len(url) > 20:
145
+ return url
146
+ # Generic smil URL
147
+ m = re.search(r"(https?://[^\s\"'<>\\]+\.smil[^\s\"'<>\\]*)", html, re.IGNORECASE)
148
  if m:
149
  url = m.group(1).strip()
150
  if len(url) > 20:
 
154
 
155
  def fetch_xemtv_stream(channel_id):
156
  """Scrape xemtv.net PHP page for fresh stream URL."""
157
+ php_url = XEMTV_PHP.get(channel_id)
158
  if not php_url:
159
  return None
160
  try:
161
+ r = requests.get(php_url, headers=UA, timeout=10, allow_redirects=True)
162
  if r.status_code == 200:
163
+ url = extract_m3u8_from_html(r.text)
164
+ if url and _verify_url(url):
165
+ return url
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
166
  except:
167
  pass
168
  return None
 
170
 
171
  def fetch_vtv_stream(channel_id):
172
  """
173
+ Fetch m3u8 stream URL with multi-source failover.
174
+ Returns (url, num_sources_tried) or (None, 0).
 
 
 
175
  """
176
  channel_id = channel_id.lower().strip()
177
  name_map = {
178
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
179
  'vtv_can_tho': 'vtv10', 'cantho': 'vtv10',
 
 
 
180
  }
181
  channel_id = name_map.get(channel_id, channel_id)
182
 
 
184
  if cached is not None:
185
  return cached
186
 
187
+ sources_tried = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
188
 
189
+ # 1. Try xemtv.net scraping first (fresh, dynamic)
190
  xemtv_url = fetch_xemtv_stream(channel_id)
191
  if xemtv_url:
192
+ result = {"url": xemtv_url, "source": "xemtv", "all_sources": [xemtv_url]}
193
+ _set_cache(channel_id, result)
194
+ return result
195
 
196
+ # 2. Try all static sources in order
197
+ static_sources = CHANNEL_SOURCES.get(channel_id, [])
198
+ for url in static_sources:
199
+ sources_tried.append(url)
200
+ if _verify_url(url):
201
+ result = {"url": url, "source": "static", "all_sources": static_sources}
202
+ _set_cache(channel_id, result)
203
+ return result
204
 
205
+ # 3. VTVPrime special: no free sources
206
+ result = {"url": None, "source": "none", "all_sources": []}
207
+ _set_cache(channel_id, result)
208
+ return result
209
 
210
 
211
  @router.get("/api/vtv/streams")
212
  def api_vtv_streams():
213
+ """Get all VTV channel stream URLs with multi-source info."""
214
  result = {}
215
  for ch_id in CHANNEL_NAMES:
216
+ stream = fetch_vtv_stream(ch_id)
217
  result[ch_id] = {
218
  "name": CHANNEL_NAMES[ch_id],
219
+ "stream_url": stream.get("url") if stream else None,
220
+ "source": stream.get("source", "none") if stream else "none",
221
+ "all_sources": stream.get("all_sources", []) if stream else [],
222
+ "status": "ok" if stream and stream.get("url") else "offline",
223
  }
224
  return JSONResponse(result)
225
 
 
227
  @router.get("/api/vtv/stream/{channel_id}")
228
  def api_vtv_stream(channel_id: str):
229
  """Get stream URL for a specific VTV channel."""
230
+ stream = fetch_vtv_stream(channel_id)
231
+ if stream and stream.get("url"):
232
+ return JSONResponse({"stream_url": stream["url"], "source": stream["source"], "status": "ok"})
233
  return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
234
 
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  @router.get("/api/proxy/m3u8/vtv")
237
  def proxy_vtv_m3u8(url: str = Query(...)):
238
+ """Proxy an m3u8 stream URL with proper headers."""
239
  try:
240
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
241
  if "fptplay" in url:
242
  headers["Referer"] = "https://fptplay.vn/"
243
  headers["Origin"] = "https://fptplay.vn"
244
+ elif "vtvgolive" in url or "vtvdigital" in url:
245
+ headers["Referer"] = "https://vtvgo.vn/"
246
  elif "xemtv" in url:
247
  headers["Referer"] = "https://hd.xemtv.net/"
248
+
249
  r = requests.get(url, headers=headers, timeout=15, allow_redirects=True)
250
  if r.status_code != 200:
251
+ return Response(status_code=502, content=f"upstream error: {r.status_code}")
252
+
253
  content = r.text
254
  lines = content.split('\n')
255
  rewritten = []
256
  base_url = url.rsplit('/', 1)[0] + '/'
257
+
258
  for line in lines:
259
  line = line.strip()
260
  if not line or line.startswith('#'):
 
264
  if not seg_url.startswith('http'):
265
  seg_url = base_url + seg_url
266
  rewritten.append("/api/proxy/seg/vtv?url=" + requests.utils.quote(seg_url, safe=""))
267
+
268
  return Response(
269
  content='\n'.join(rewritten).encode('utf-8'),
270
  media_type="application/vnd.apple.mpegurl",
 
276
 
277
  @router.get("/api/proxy/seg/vtv")
278
  def proxy_vtv_segment(url: str = Query(...)):
279
+ """Proxy a video segment with proper headers."""
280
  try:
281
  headers = {"User-Agent": UA["User-Agent"], "Accept": "*/*"}
282
  if "fptplay" in url:
283
  headers["Referer"] = "https://fptplay.vn/"
284
  headers["Origin"] = "https://fptplay.vn"
285
+ elif "vtvgolive" in url or "vtvdigital" in url:
286
+ headers["Referer"] = "https://vtvgo.vn/"
287
+
288
+ r = requests.get(url, headers=headers, timeout=30, allow_redirects=True, stream=True)
289
  if r.status_code != 200:
290
+ return Response(status_code=502, content=f"upstream error: {r.status_code}")
291
+
292
  data = r.content
293
+ # Strip PNG header if present (some CDNs prepend it)
294
  if len(data) > 188 and data[0:4] == b'\x89PNG' and data[188] == 0x47:
295
  data = data[188:]
296
+
297
  return Response(
298
  content=data,
299
  media_type="video/mp2t",