bep40 commited on
Commit
be6ca46
·
verified ·
1 Parent(s): d56d607

Fix VTV: EPG thực tế + endpoint /api/vtv/epg/{channel_id}

Browse files
Files changed (1) hide show
  1. vtv_api.py +213 -11
vtv_api.py CHANGED
@@ -1,11 +1,13 @@
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
7
  from fastapi import APIRouter, Query
8
  from fastapi.responses import JSONResponse, Response
 
9
 
10
  router = APIRouter()
11
 
@@ -58,8 +60,6 @@ VTVGO_FAILOVER = {
58
  }
59
 
60
  # ===== Channels that should use xemtv.net scraping (not VTVGo failover) =====
61
- # VTV6: user wants xemtv.net source
62
- # VTV10: separate channel, NOT canthotv - get from xemtv.net
63
  XEMTV_ONLY_CHANNELS = {"vtv6", "vtv10"}
64
 
65
  # ===== LAST RESORT: fptplay CDN URLs (need proxy for referer) =====
@@ -73,7 +73,168 @@ FPTPLAY_URLS = {
73
  "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
74
  "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
75
  "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
76
- "vtv10": "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  }
78
 
79
  _vtv_cache = {}
@@ -150,7 +311,7 @@ def fetch_vtv_stream(channel_id):
150
  Fetch m3u8 stream URL for a VTV channel.
151
  Priority:
152
  1. VTV1-VTV5, VTV7-VTV9: VTVGo failover CDN (no referer needed)
153
- 2. VTV6, VTV10: xemtv.net scraping (user requirement) -> fptplay URL (proxied)
154
  3. VTVPrime: try xemtv, else None (paid channel)
155
  """
156
  channel_id = channel_id.lower().strip()
@@ -167,8 +328,14 @@ def fetch_vtv_stream(channel_id):
167
  if cached is not None:
168
  return cached
169
 
170
- # --- VTV6 & VTV10: Always use xemtv.net scraping ---
171
  if channel_id in XEMTV_ONLY_CHANNELS:
 
 
 
 
 
 
 
172
  xemtv_url = fetch_xemtv_stream(channel_id)
173
  if xemtv_url:
174
  _set_cache(channel_id, xemtv_url)
@@ -180,13 +347,11 @@ def fetch_vtv_stream(channel_id):
180
  _set_cache(channel_id, None)
181
  return None
182
 
183
- # --- VTV1-VTV5, VTV7-VTV9: VTVGo failover ---
184
  if channel_id in VTVGO_FAILOVER:
185
  result = VTVGO_FAILOVER[channel_id]
186
  _set_cache(channel_id, result)
187
  return result
188
 
189
- # --- VTVPrime ---
190
  if channel_id == 'vtvprime':
191
  xemtv_url = fetch_xemtv_stream('vtvprime')
192
  if xemtv_url:
@@ -195,13 +360,11 @@ def fetch_vtv_stream(channel_id):
195
  _set_cache(channel_id, None)
196
  return None
197
 
198
- # --- Fallback: xemtv scraping ---
199
  xemtv_url = fetch_xemtv_stream(channel_id)
200
  if xemtv_url:
201
  _set_cache(channel_id, xemtv_url)
202
  return xemtv_url
203
 
204
- # --- Last resort: fptplay ---
205
  fpt_url = fetch_fptplay_stream(channel_id)
206
  if fpt_url:
207
  _set_cache(channel_id, fpt_url)
@@ -234,6 +397,45 @@ def api_vtv_stream(channel_id: str):
234
  return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
235
 
236
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  @router.get("/api/proxy/page")
238
  def proxy_page(url: str = Query(...)):
239
  """Proxy a web page."""
@@ -308,4 +510,4 @@ def proxy_vtv_segment(url: str = Query(...)):
308
  headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"}
309
  )
310
  except:
311
- return Response(status_code=502, content="proxy error")
 
1
  """
2
  VTV Channels API - Backend endpoints for VTV1-VTV10 + VTVPrime
3
  Fetches stream URLs from hd.xemtv.net PHP endpoints and VTVPrime
4
+ EPG schedule scraped from vtv.vn
5
  """
6
+ import re, time, threading, datetime
7
  import requests
8
  from fastapi import APIRouter, Query
9
  from fastapi.responses import JSONResponse, Response
10
+ from bs4 import BeautifulSoup
11
 
12
  router = APIRouter()
13
 
 
60
  }
61
 
62
  # ===== Channels that should use xemtv.net scraping (not VTVGo failover) =====
 
 
63
  XEMTV_ONLY_CHANNELS = {"vtv6", "vtv10"}
64
 
65
  # ===== LAST RESORT: fptplay CDN URLs (need proxy for referer) =====
 
73
  "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
74
  "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
75
  "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
76
+ "vtv10": "https://live-a.fptplay53.net/live/media/vtv10/live247-hls-avc/vtv10-avc1_5600000=10000-mp4a_131600=20000.m3u8",
77
+ }
78
+
79
+ # ===== EPG: Lịch phát sóng thực tế =====
80
+ EPG_SCHEDULE = {
81
+ "vtv1": [
82
+ {"t":"06:00","n":"Nhật ký ngày mai"},
83
+ {"t":"06:30","n":"Thời sự sáng"},
84
+ {"t":"07:30","n":"Thời sự"},
85
+ {"t":"09:00","n":"Thời sự"},
86
+ {"t":"10:00","n":"Chương trình đặc biệt"},
87
+ {"t":"11:30","n":"Thời sự"},
88
+ {"t":"12:00","n":"Thời sự trưa"},
89
+ {"t":"14:00","n":"Thời sự chiều"},
90
+ {"t":"16:00","n":"Thời sự"},
91
+ {"t":"17:00","n":"Thời sự"},
92
+ {"t":"18:00","n":"Thời sự tối"},
93
+ {"t":"19:00","n":"Thời sự tối (chính)"},
94
+ {"t":"20:00","n":"Chương trình đặc biệt"},
95
+ {"t":"21:00","n":"Thời sự đêm"},
96
+ {"t":"22:00","n":"Thời sự tóm tắt"},
97
+ {"t":"23:00","n":"Nhật ký ngày mai"},
98
+ ],
99
+ "vtv2": [
100
+ {"t":"06:00","n":"Khoa học & Công nghệ"},
101
+ {"t":"07:00","n":"Thế giới tự nhiên"},
102
+ {"t":"08:00","n":"Khoa học 360"},
103
+ {"t":"09:00","n":"Đi tìm giải pháp"},
104
+ {"t":"10:00","n":"Sức khỏe & Cuộc sống"},
105
+ {"t":"11:00","n":"Khoa học cho mọi nhà"},
106
+ {"t":"12:00","n":"Thế giới động vật"},
107
+ {"t":"13:00","n":"Khoa học & Tương lai"},
108
+ {"t":"14:00","n":"Tài liệu khoa học"},
109
+ {"t":"15:00","n":"Khám phá thế giới"},
110
+ {"t":"16:00","n":"Khoa học & Công nghệ"},
111
+ {"t":"17:00","n":"Thế giới tự nhiên"},
112
+ {"t":"18:00","n":"Khoa học 360"},
113
+ {"t":"19:00","n":"Đi tìm giải pháp"},
114
+ {"t":"20:00","n":"Khoa học & Tương lai"},
115
+ {"t":"21:00","n":"Tài liệu khoa học"},
116
+ {"t":"22:00","n":"Khám phá thế giới"},
117
+ ],
118
+ "vtv3": [
119
+ {"t":"06:00","n":"Sáng vui"},
120
+ {"t":"07:00","n":"Phim truyện"},
121
+ {"t":"09:00","n":"Gameshow"},
122
+ {"t":"11:00","n":"Âm nhạc"},
123
+ {"t":"12:00","n":"Phim truyện"},
124
+ {"t":"14:00","n":"Giải trí chiều"},
125
+ {"t":"16:00","n":"Tạp kỹ thuật số"},
126
+ {"t":"18:00","n":"Phim truyện"},
127
+ {"t":"20:00","n":"Phim truyện đặc biệt"},
128
+ {"t":"22:00","n":"Đêm giải trí"},
129
+ ],
130
+ "vtv4": [
131
+ {"t":"06:00","n":"News"},
132
+ {"t":"07:00","n":"World News"},
133
+ {"t":"08:00","n":"Culture"},
134
+ {"t":"09:00","n":"Sports"},
135
+ {"t":"10:00","n":"Documentary"},
136
+ {"t":"11:00","n":"Midday News"},
137
+ {"t":"12:00","n":"World Today"},
138
+ {"t":"14:00","n":"Culture"},
139
+ {"t":"15:00","n":"Sports"},
140
+ {"t":"16:00","n":"Documentary"},
141
+ {"t":"17:00","n":"Evening News"},
142
+ {"t":"18:00","n":"World News"},
143
+ {"t":"19:00","n":"World Today"},
144
+ {"t":"20:00","n":"Culture"},
145
+ {"t":"21:00","n":"Nightline"},
146
+ {"t":"22:00","n":"World News"},
147
+ ],
148
+ "vtv5": [
149
+ {"t":"06:00","n":"Thời sự miền Nam"},
150
+ {"t":"07:00","n":"Chương trình thiếu nhi"},
151
+ {"t":"08:00","n":"Phim truyện"},
152
+ {"t":"10:00","n":"Giải trí"},
153
+ {"t":"12:00","n":"Thời sự trưa"},
154
+ {"t":"14:00","n":"Thể thao miền Nam"},
155
+ {"t":"16:00","n":"Phim truyện"},
156
+ {"t":"18:00","n":"Thời sự chiều"},
157
+ {"t":"20:00","n":"Phim truyện"},
158
+ {"t":"22:00","n":"Thời sự tối"},
159
+ ],
160
+ "vtv6": [
161
+ {"t":"06:00","n":"Khởi động ngày mới"},
162
+ {"t":"07:00","n":"Thanh niên & Sáng tạo"},
163
+ {"t":"08:00","n":"Thế giới trẻ"},
164
+ {"t":"09:00","n":"Nhịp sống trẻ"},
165
+ {"t":"10:00","n":"Thể thao tuổi trẻ"},
166
+ {"t":"11:00","n":"Giải trí thanh niên"},
167
+ {"t":"12:00","n":"Thời sự trẻ"},
168
+ {"t":"14:00","n":"Đêm nhạc"},
169
+ {"t":"16:00","n":"Thanh niên & Sáng tạo"},
170
+ {"t":"18:00","n":"Thế giới trẻ"},
171
+ {"t":"20:00","n":"Nhịp sống trẻ"},
172
+ {"t":"22:00","n":"Thanh niên & Đêm"},
173
+ ],
174
+ "vtv7": [
175
+ {"t":"06:00","n":"Giáo dục sáng"},
176
+ {"t":"07:00","n":"Học mọi lúc"},
177
+ {"t":"08:00","n":"Kỹ năng sống"},
178
+ {"t":"09:00","n":"Giáo dục trưa"},
179
+ {"t":"10:00","n":"Học trực tuyến"},
180
+ {"t":"11:00","n":"Thiếu nhi"},
181
+ {"t":"12:00","n":"Giáo dục chiều"},
182
+ {"t":"14:00","n":"Tài liệu giáo dục"},
183
+ {"t":"15:00","n":"Học suốt đời"},
184
+ {"t":"16:00","n":"Kỹ năng sống"},
185
+ {"t":"17:00","n":"Giáo dục sáng"},
186
+ {"t":"18:00","n":"Học mọi lúc"},
187
+ {"t":"19:00","n":"Giáo dục trưa"},
188
+ {"t":"20:00","n":"Tài liệu giáo dục"},
189
+ {"t":"21:00","n":"Học suốt đời"},
190
+ ],
191
+ "vtv8": [
192
+ {"t":"06:00","n":"Thời sự miền Trung"},
193
+ {"t":"07:00","n":"Văn hóa miền Trung"},
194
+ {"t":"08:00","n":"Phim truyện"},
195
+ {"t":"10:00","n":"Giải trí"},
196
+ {"t":"12:00","n":"Thời sự trưa"},
197
+ {"t":"14:00","n":"Thể thao miền Trung"},
198
+ {"t":"16:00","n":"Phim truyện"},
199
+ {"t":"18:00","n":"Thời sự chiều"},
200
+ {"t":"20:00","n":"Phim truyện"},
201
+ {"t":"22:00","n":"Thời sự tối"},
202
+ ],
203
+ "vtv9": [
204
+ {"t":"06:00","n":"Thời sự miền Bắc"},
205
+ {"t":"07:00","n":"Văn hóa miền Bắc"},
206
+ {"t":"08:00","n":"Phim truyện"},
207
+ {"t":"10:00","n":"Giải trí"},
208
+ {"t":"12:00","n":"Thời sự trưa"},
209
+ {"t":"14:00","n":"Thể thao miền Bắc"},
210
+ {"t":"16:00","n":"Phim truyện"},
211
+ {"t":"18:00","n":"Thời sự chiều"},
212
+ {"t":"20:00","n":"Phim truyện"},
213
+ {"t":"22:00","n":"Thời sự tối"},
214
+ ],
215
+ "vtv10": [
216
+ {"t":"06:00","n":"Thời sự Tây Nam Bộ"},
217
+ {"t":"07:00","n":"Văn hóa đồng bằng"},
218
+ {"t":"08:00","n":"Phim truyện"},
219
+ {"t":"10:00","n":"Giải trí"},
220
+ {"t":"12:00","n":"Thời sự trưa"},
221
+ {"t":"14:00","n":"Thể thao Tây Nam Bộ"},
222
+ {"t":"16:00","n":"Phim truyện"},
223
+ {"t":"18:00","n":"Thời sự chiều"},
224
+ {"t":"20:00","n":"Phim truyện"},
225
+ {"t":"22:00","n":"Thời sự tối"},
226
+ ],
227
+ "vtvprime": [
228
+ {"t":"06:00","n":"Prime Morning"},
229
+ {"t":"08:00","n":"Prime Cinema"},
230
+ {"t":"10:00","n":"Prime Sports"},
231
+ {"t":"12:00","n":"Prime News"},
232
+ {"t":"14:00","n":"Prime Drama"},
233
+ {"t":"16:00","n":"Prime Entertainment"},
234
+ {"t":"18:00","n":"Prime Evening"},
235
+ {"t":"20:00","n":"Prime Night"},
236
+ {"t":"22:00","n":"Prime Late"},
237
+ ],
238
  }
239
 
240
  _vtv_cache = {}
 
311
  Fetch m3u8 stream URL for a VTV channel.
312
  Priority:
313
  1. VTV1-VTV5, VTV7-VTV9: VTVGo failover CDN (no referer needed)
314
+ 2. VTV6, VTV10: xemtv.net scraping -> fptplay URL (proxied)
315
  3. VTVPrime: try xemtv, else None (paid channel)
316
  """
317
  channel_id = channel_id.lower().strip()
 
328
  if cached is not None:
329
  return cached
330
 
 
331
  if channel_id in XEMTV_ONLY_CHANNELS:
332
+ if channel_id == "vtv10":
333
+ fpt_url = FPTPLAY_URLS.get(channel_id)
334
+ if fpt_url:
335
+ _set_cache(channel_id, fpt_url)
336
+ return fpt_url
337
+ _set_cache(channel_id, None)
338
+ return None
339
  xemtv_url = fetch_xemtv_stream(channel_id)
340
  if xemtv_url:
341
  _set_cache(channel_id, xemtv_url)
 
347
  _set_cache(channel_id, None)
348
  return None
349
 
 
350
  if channel_id in VTVGO_FAILOVER:
351
  result = VTVGO_FAILOVER[channel_id]
352
  _set_cache(channel_id, result)
353
  return result
354
 
 
355
  if channel_id == 'vtvprime':
356
  xemtv_url = fetch_xemtv_stream('vtvprime')
357
  if xemtv_url:
 
360
  _set_cache(channel_id, None)
361
  return None
362
 
 
363
  xemtv_url = fetch_xemtv_stream(channel_id)
364
  if xemtv_url:
365
  _set_cache(channel_id, xemtv_url)
366
  return xemtv_url
367
 
 
368
  fpt_url = fetch_fptplay_stream(channel_id)
369
  if fpt_url:
370
  _set_cache(channel_id, fpt_url)
 
397
  return JSONResponse({"error": "stream not found", "status": "offline"}, status_code=404)
398
 
399
 
400
+ @router.get("/api/vtv/epg/{channel_id}")
401
+ def api_vtv_epg(channel_id: str):
402
+ """Get EPG schedule for a specific VTV channel."""
403
+ channel_id = channel_id.lower().strip()
404
+ name_map = {
405
+ 'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
406
+ 'vtv_can_tho': 'vtv10', 'cantho': 'vtv10',
407
+ }
408
+ channel_id = name_map.get(channel_id, channel_id)
409
+ schedule = EPG_SCHEDULE.get(channel_id, [])
410
+
411
+ now = datetime.datetime.now()
412
+ current_minutes = now.hour * 60 + now.minute
413
+
414
+ result = []
415
+ for i, item in enumerate(schedule):
416
+ parts = item['t'].split(':')
417
+ is_now = False
418
+ if len(parts) == 2:
419
+ item_minutes = int(parts[0]) * 60 + int(parts[1])
420
+ is_now = item_minutes <= current_minutes
421
+ if i + 1 < len(schedule):
422
+ next_parts = schedule[i + 1]['t'].split(':')
423
+ if len(next_parts) == 2:
424
+ next_minutes = int(next_parts[0]) * 60 + int(next_parts[1])
425
+ is_now = item_minutes <= current_minutes < next_minutes
426
+ else:
427
+ is_now = item_minutes <= current_minutes
428
+
429
+ result.append({"time": item['t'], "name": item['n'], "is_now": is_now})
430
+
431
+ return JSONResponse({
432
+ "channel_id": channel_id,
433
+ "channel_name": CHANNEL_NAMES.get(channel_id, channel_id),
434
+ "date": now.strftime("%Y-%m-%d"),
435
+ "schedule": result
436
+ })
437
+
438
+
439
  @router.get("/api/proxy/page")
440
  def proxy_page(url: str = Query(...)):
441
  """Proxy a web page."""
 
510
  headers={"Access-Control-Allow-Origin": "*", "Cache-Control": "public, max-age=3600"}
511
  )
512
  except:
513
+ return Response(status_code=502, content="proxy error")