bep40 commited on
Commit
48e2e2a
·
verified ·
1 Parent(s): 86ddfc8

Upload vtv_api.py

Browse files
Files changed (1) hide show
  1. vtv_api.py +41 -47
vtv_api.py CHANGED
@@ -1,6 +1,7 @@
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
  Fetches stream URLs from xemtv.us PHP endpoints (primary)
 
4
  Fallback: FPTPlay CDN → VTVGo CDN → xemtv.net (legacy)
5
  EPG data scraped from https://vtv.vn/lich-phat-song.htm
6
  """
@@ -35,6 +36,20 @@ XEMTV_US_ENDPOINTS = {
35
  "vtvprime": "https://xemtv.us/tv/vtvprime.php",
36
  }
37
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
38
  # ===== LEGACY: xemtv.net (may return 403, keep as last resort) =====
39
  XEMTV_LEGACY_ENDPOINTS = {
40
  "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
@@ -135,6 +150,22 @@ def fetch_xemtv_us_stream(channel_id):
135
  pass
136
  return None
137
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  def fetch_xemtv_legacy_stream(channel_id):
139
  php_url = XEMTV_LEGACY_ENDPOINTS.get(channel_id)
140
  if not php_url:
@@ -181,7 +212,6 @@ def fetch_vtvgo_stream(channel_id):
181
  return None
182
 
183
  def normalize_fptplay_url(url):
184
- """Replace old/broken FPTPlay URLs with new working ones"""
185
  if not url:
186
  return url
187
  old_to_new = {
@@ -213,12 +243,6 @@ def normalize_fptplay_url(url):
213
  return old_to_new.get(url, url)
214
 
215
  def fetch_vtv_stream(channel_id):
216
- """Fetch VTV stream with multi-source fallback chain:
217
- 1. xemtv.us (primary - new domain, most reliable)
218
- 2. FPTPlay CDN (fallback - new URLs)
219
- 3. VTVGo CDN (fallback)
220
- 4. xemtv.net legacy (last resort)
221
- """
222
  channel_id = channel_id.lower().strip()
223
  name_map = {
224
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
@@ -233,32 +257,34 @@ def fetch_vtv_stream(channel_id):
233
  return cached
234
 
235
  if channel_id == 'vtvprime':
236
- url = fetch_xemtv_us_stream('vtvprime') or fetch_xemtv_legacy_stream('vtvprime')
237
  if url:
238
  url = normalize_fptplay_url(url)
239
  _set_cache(channel_id, url)
240
  return url
241
 
242
- # Source 1: xemtv.us (primary)
243
  url = fetch_xemtv_us_stream(channel_id)
244
  if url:
245
  url = normalize_fptplay_url(url)
246
  _set_cache(channel_id, url)
247
  return url
248
 
249
- # Source 2: FPTPlay CDN
 
 
 
 
 
250
  url = fetch_fptplay_stream(channel_id)
251
  if url:
252
  _set_cache(channel_id, url)
253
  return url
254
 
255
- # Source 3: VTVGo CDN
256
  url = fetch_vtvgo_stream(channel_id)
257
  if url:
258
  _set_cache(channel_id, url)
259
  return url
260
 
261
- # Source 4: xemtv.net legacy (last resort)
262
  url = fetch_xemtv_legacy_stream(channel_id)
263
  if url:
264
  url = normalize_fptplay_url(url)
@@ -314,8 +340,8 @@ def proxy_vtv_m3u8(url: str = Query(...)):
314
  return Response(status_code=502, content="upstream error")
315
  content = r.text
316
  lines = content.split('\n')
 
317
  rewritten = []
318
- base_url = url.rsplit('/', 1)[0] + '/'
319
  for line in lines:
320
  line = line.strip()
321
  if not line or line.startswith('#'):
@@ -351,7 +377,7 @@ def proxy_vtv_segment(url: str = Query(...)):
351
 
352
  _epg_cache = {}
353
  _epg_cache_time = 0
354
- _EPG_CACHE_TTL = 600 # Giảm từ 30 phút xuống 10 phút, đảm bảo dữ liệu mới
355
 
356
  VTV_CHANNEL_MAP = {
357
  "vtv1": "vtv1", "vtv2": "vtv2", "vtv3": "vtv3", "vtv4": "vtv4",
@@ -366,7 +392,6 @@ def _fetch_epg_from_vtv():
366
  if _epg_cache and now_ts - _epg_cache_time < _EPG_CACHE_TTL:
367
  return _epg_cache
368
  epg_data = {}
369
- # Lấy thời gian VN hiện tại để truyền vào parse_time
370
  now_vn = datetime.now(VN_TZ)
371
  try:
372
  headers = {
@@ -411,7 +436,6 @@ def _fetch_epg_from_vtv():
411
  title = title_span.get_text(strip=True)
412
  if not time_str or not title:
413
  continue
414
- # Truyền reference_date để xử lý quy tắc ngày truyền hình VTV
415
  start_dt = _parse_time(time_str, reference_date=now_vn)
416
  if not start_dt:
417
  continue
@@ -433,16 +457,6 @@ def _fetch_epg_from_vtv():
433
  return epg_data
434
 
435
  def _parse_time(time_str, reference_date=None):
436
- """
437
- Parse giờ từ lịch phát sóng VTV (đã là giờ VN UTC+7) sang datetime có timezone.
438
-
439
- VTV hiển thị lịch theo ngày dương lịch (không phải ngày truyền hình).
440
- Ví dụ: Lịch ngày 17/06 sẽ hiển thị tất cả chương trình từ 00:00 đến 23:59 ngày 17/06.
441
-
442
- Logic:
443
- - Giờ 00:00-04:59: Có thể là đêm khuya của ngày hôm trước HOẶC sáng sớm của ngày mới
444
- - Giờ 05:00-23:59: Luôn thuộc ngày hiện tại
445
- """
446
  if not time_str:
447
  return None
448
  time_str = time_str.strip().replace("h", ":").replace("H", ":")
@@ -450,35 +464,20 @@ def _parse_time(time_str, reference_date=None):
450
  if m:
451
  try:
452
  hour, minute = int(m.group(1)), int(m.group(2))
453
-
454
  if reference_date:
455
  base_date = reference_date
456
  else:
457
- # Lấy ngày hiện tại theo giờ VN (UTC+7)
458
  now_vn = datetime.now(VN_TZ)
459
  base_date = now_vn
460
-
461
  from datetime import timedelta
462
-
463
- # Xác định ngày cho giờ program
464
  if hour < 5:
465
- # Giờ 00:00-04:59: Cần xem giờ hiện tại để quyết định
466
  if base_date.hour < 5:
467
- # Nếu hiện tại cũng < 5:00 (đang trong khoảng sáng sớm)
468
- # → Giờ program thuộc CÙNG NGÀY với giờ hiện tại
469
  tv_date = base_date
470
  else:
471
- # Nếu hiện tại >= 5:00 (đã qua sáng sớm)
472
- # → Giờ 00:00-04:59 thuộc NGÀY HÔM TRƯỚC
473
  tv_date = base_date - timedelta(days=1)
474
  else:
475
- # Giờ 05:00-23:59: Luôn thuộc ngày hiện tại
476
  tv_date = base_date
477
-
478
- # Tạo datetime với giờ từ lịch
479
  result = tv_date.replace(hour=hour, minute=minute, second=0, microsecond=0)
480
-
481
- # Đảm bảo result có timezone
482
  if result.tzinfo is None:
483
  result = result.replace(tzinfo=VN_TZ)
484
  return result
@@ -494,18 +493,13 @@ def _get_epg_for_channel(channel_id):
494
  now = datetime.now(VN_TZ)
495
  today = now.date()
496
  result = []
497
-
498
- # Lọc chỉ lấy programs của ngày hôm nay (theo lịch VTV)
499
  today_programmes = []
500
  for p in programmes:
501
  start_dt = p.get("start_dt")
502
  if start_dt and start_dt.date() == today:
503
  today_programmes.append(p)
504
-
505
- # Nếu không có programs cho hôm nay, dùng tất cả (fallback)
506
  if not today_programmes:
507
  today_programmes = programmes
508
-
509
  for i, p in enumerate(today_programmes):
510
  start_dt = p.get("start_dt")
511
  stop_dt = None
@@ -543,4 +537,4 @@ def api_vtv_epg_refresh():
543
  return JSONResponse({
544
  "status": "refreshed", "channels": len(epg_data),
545
  "total_programmes": sum(len(v) for v in epg_data),
546
- })
 
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
  """
 
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",
43
+ "vtv3": "https://www.xemtivitop.com/2018/10/vtv3-online.html?m=1",
44
+ "vtv4": "https://www.xemtivitop.com/2018/10/vtv4-online.html?m=1",
45
+ "vtv5": "https://www.xemtivitop.com/2018/10/vtv5-online.html?m=1",
46
+ "vtv6": "https://www.xemtivitop.com/2018/10/vtv6-online.html?m=1",
47
+ "vtv7": "https://www.xemtivitop.com/2018/10/vtv7-online.html?m=1",
48
+ "vtv8": "https://www.xemtivitop.com/2018/10/vtv8-online.html?m=1",
49
+ "vtv9": "https://www.xemtivitop.com/2018/10/vtv9-online.html?m=1",
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",
 
150
  pass
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
158
+ try:
159
+ headers = {**UA, "Referer": "https://www.xemtivitop.com/"}
160
+ r = requests.get(page_url, headers=headers, timeout=15, allow_redirects=True, verify=False)
161
+ if r.status_code == 200:
162
+ m3u8 = extract_m3u8_from_html(r.text)
163
+ if m3u8:
164
+ return m3u8
165
+ except:
166
+ pass
167
+ return None
168
+
169
  def fetch_xemtv_legacy_stream(channel_id):
170
  php_url = XEMTV_LEGACY_ENDPOINTS.get(channel_id)
171
  if not php_url:
 
212
  return None
213
 
214
  def normalize_fptplay_url(url):
 
215
  if not url:
216
  return url
217
  old_to_new = {
 
243
  return old_to_new.get(url, url)
244
 
245
  def fetch_vtv_stream(channel_id):
 
 
 
 
 
 
246
  channel_id = channel_id.lower().strip()
247
  name_map = {
248
  'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
 
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)
 
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('#'):
 
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",
 
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 = {
 
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
 
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", ":")
 
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
 
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
 
537
  return JSONResponse({
538
  "status": "refreshed", "channels": len(epg_data),
539
  "total_programmes": sum(len(v) for v in epg_data),
540
+ })