bep40 commited on
Commit
6e61354
·
verified ·
1 Parent(s): eeddff8

Switch to YouTube embed for live TV - 100% reliable, no token needed

Browse files
Files changed (1) hide show
  1. main.py +14 -125
main.py CHANGED
@@ -15,11 +15,9 @@ BASE_24H = "https://www.24h.com.vn"
15
  SPACE_URL = "https://bep40-vnews.hf.space"
16
  _cache = {}
17
  _cache_ttl = 300
18
- _cache_ttl_tv = 1800 # 30 min cache for TV streams
19
- def _cached(key, fn, ttl=None):
20
  now=time.time()
21
- t=ttl or _cache_ttl
22
- if key in _cache and now-_cache[key]["t"]<t:return _cache[key]["d"]
23
  try:data=fn()
24
  except:data=_cache.get(key,{}).get("d",[])
25
  _cache[key]={"d":data,"t":now};return data
@@ -27,117 +25,21 @@ def _get(url,headers=None):
27
  h=headers or HEADERS;r=requests.get(url,headers=h,timeout=15);r.encoding="utf-8"
28
  return BeautifulSoup(r.text,"lxml")
29
 
30
- # ===== VTV LIVE TV STREAMING =====
 
31
  VTV_CHANNELS = [
32
- {"id": 1, "name": "VTV1 HD", "slug": "vtv1", "desc": "Thời sự · Chính trị", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv1.png"},
33
- {"id": 2, "name": "VTV2 HD", "slug": "vtv2", "desc": "Khoa học · Giáo dục", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv2.png"},
34
- {"id": 3, "name": "VTV3 HD", "slug": "vtv3", "desc": "Giải trí · Thể thao", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv3.png"},
35
- {"id": 5, "name": "VTV5 HD", "slug": "vtv5", "desc": "Dân tộc · Miền núi", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv5.png"},
36
- {"id": 6, "name": "VTV6 HD", "slug": "vtv6", "desc": "Thanh thiếu niên", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv6.png"},
37
- {"id": 7, "name": "VTV7 HD", "slug": "vtv7", "desc": "Giáo dục quốc gia", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv7.png"},
38
- {"id": 8, "name": "VTV8 HD", "slug": "vtv8", "desc": "Miền Trung · Tây Nguyên", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv8.png"},
39
- {"id": 9, "name": "VTV9 HD", "slug": "vtv9", "desc": "Nam Bộ · TP.HCM", "logo": "https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv9.png"},
40
  ]
41
 
42
- def get_vtvgo_stream(channel_id):
43
- """Extract live stream URL from VTVgo for a given channel."""
44
- try:
45
- session = requests.Session()
46
- # Step 1: Visit channel page to get cookies/tokens
47
- channel_url = f"https://vtvgo.vn/channel/vtv{channel_id}-1,{channel_id}.html"
48
- headers = {
49
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
50
- "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
51
- "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
52
- "Referer": "https://vtvgo.vn/",
53
- }
54
- r = session.get(channel_url, headers=headers, timeout=15)
55
-
56
- # Try to find stream URL patterns in page source
57
- # Pattern 1: Direct m3u8 URL
58
- m3u8_urls = re.findall(r'(https?://[^\s"\'<>\\]+\.m3u8[^\s"\'<>\\]*)', r.text)
59
- if m3u8_urls:
60
- return m3u8_urls[0].replace('\\/', '/')
61
-
62
- # Pattern 2: Look for token/stream in JS variables
63
- # VTVgo often has: var token = "..."; var stream_url = "...";
64
- token_match = re.search(r'(?:token|TOKEN)\s*[=:]\s*["\']([^"\']+)["\']', r.text)
65
- url_match = re.search(r'(?:url|link|src|stream_url|hlsUrl|videoUrl)\s*[=:]\s*["\']([^"\']*m3u8[^"\']*)["\']', r.text)
66
- if url_match:
67
- stream = url_match.group(1).replace('\\/', '/')
68
- if token_match:
69
- sep = '&' if '?' in stream else '?'
70
- stream += f"{sep}token={token_match.group(1)}"
71
- return stream
72
-
73
- # Pattern 3: Look for API endpoint to get stream
74
- api_match = re.search(r'(?:getStream|getLink|getLive|getChannel)\s*\(\s*["\']?(\d+)', r.text)
75
- if api_match:
76
- # Try calling the API
77
- api_urls = [
78
- f"https://vtvgo.vn/ajax-get-stream?channel_id={channel_id}",
79
- f"https://package.vtvgo.vn/api/v2/get_list_channel?channel_id={channel_id}",
80
- ]
81
- for api_url in api_urls:
82
- try:
83
- ar = session.get(api_url, headers={**headers, "X-Requested-With": "XMLHttpRequest"}, timeout=10)
84
- if ar.status_code == 200:
85
- data = ar.json() if ar.headers.get('content-type','').startswith('application/json') else {}
86
- stream = data.get('stream_url') or data.get('url') or data.get('link')
87
- if stream:
88
- return stream
89
- except:
90
- continue
91
-
92
- # Pattern 4: POST-based token request (common VTVgo pattern)
93
- # Look for CSRF token or data-id
94
- csrf = re.search(r'name="csrf[_-]token"\s+content="([^"]+)"', r.text) or \
95
- re.search(r'csrf[_-]token["\s:]+["\']([^"\']+)', r.text)
96
- data_id = re.search(r'data-id\s*=\s*["\'](\d+)', r.text)
97
-
98
- if csrf or data_id:
99
- post_data = {"channel_id": channel_id, "type": "channel"}
100
- if csrf:
101
- post_data["_token"] = csrf.group(1)
102
- try:
103
- pr = session.post("https://vtvgo.vn/ajax-get-stream",
104
- data=post_data,
105
- headers={**headers, "X-Requested-With": "XMLHttpRequest", "Referer": channel_url},
106
- timeout=10)
107
- if pr.status_code == 200:
108
- try:
109
- jd = pr.json()
110
- return jd.get('stream_url') or jd.get('url')
111
- except:
112
- m = re.search(r'(https?://[^\s"\'<>\\]+\.m3u8[^\s"\'<>\\]*)', pr.text)
113
- if m: return m.group(1).replace('\\/', '/')
114
- except:
115
- pass
116
-
117
- return None
118
- except:
119
- return None
120
-
121
- def get_all_vtv_streams():
122
- """Get stream URLs for all VTV channels."""
123
- channels = []
124
- def _fetch(ch):
125
- stream = get_vtvgo_stream(ch["id"])
126
- return {**ch, "stream": stream}
127
-
128
- with ThreadPoolExecutor(4) as ex:
129
- futures = {ex.submit(_fetch, ch): ch for ch in VTV_CHANNELS}
130
- for f in as_completed(futures):
131
- try:
132
- channels.append(f.result())
133
- except:
134
- channels.append({**futures[f], "stream": None})
135
-
136
- # Sort by channel id
137
- channels.sort(key=lambda x: x["id"])
138
- return channels
139
-
140
- # ===== END VTV LIVE TV =====
141
 
142
  def scrape_vne(cat_url):
143
  try:
@@ -373,18 +275,6 @@ def api_highlights():return JSONResponse(_cached("highlights",scrape_24h_highlig
373
  def api_shorts():return JSONResponse(_cached("shorts",scrape_24h_shorts))
374
  @app.get("/api/bdp_videos")
375
  def api_bdp_videos():return JSONResponse(_cached("bdp_videos",scrape_bdp_videos))
376
- @app.get("/api/livetv")
377
- def api_livetv():
378
- """Return list of VTV live channels with stream URLs."""
379
- return JSONResponse(_cached("livetv", get_all_vtv_streams, ttl=_cache_ttl_tv))
380
- @app.get("/api/livetv/{channel_id}")
381
- def api_livetv_channel(channel_id: int):
382
- """Get stream URL for a single VTV channel (fresh, no cache)."""
383
- stream = get_vtvgo_stream(channel_id)
384
- ch = next((c for c in VTV_CHANNELS if c["id"] == channel_id), None)
385
- if ch:
386
- return JSONResponse({**ch, "stream": stream})
387
- return JSONResponse({"error": "Channel not found"}, status_code=404)
388
  @app.get("/api/video_url")
389
  def api_video_url(url:str=Query(...)):
390
  if"24h.com.vn" in url:v=extract_video_url(url)
@@ -399,7 +289,6 @@ def api_article(url:str=Query(...)):
399
  return JSONResponse(data if data else{"error":"not supported"})
400
  @app.get("/v")
401
  async def video_share(url:str=Query(default=""),title:str=Query(default="VNEWS Video"),img:str=Query(default=""),type:str=Query(default="highlights")):
402
- """Direct video share link - opens video player on VNEWS."""
403
  og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
404
  decoded_url=unquote(url)
405
  decoded_title=unquote(title)
 
15
  SPACE_URL = "https://bep40-vnews.hf.space"
16
  _cache = {}
17
  _cache_ttl = 300
18
+ def _cached(key, fn):
 
19
  now=time.time()
20
+ if key in _cache and now-_cache[key]["t"]<_cache_ttl:return _cache[key]["d"]
 
21
  try:data=fn()
22
  except:data=_cache.get(key,{}).get("d",[])
23
  _cache[key]={"d":data,"t":now};return data
 
25
  h=headers or HEADERS;r=requests.get(url,headers=h,timeout=15);r.encoding="utf-8"
26
  return BeautifulSoup(r.text,"lxml")
27
 
28
+ # ===== LIVE TV - YouTube channels (official VTV) =====
29
+ # These are official VTV YouTube channels that livestream 24/7
30
  VTV_CHANNELS = [
31
+ {"id":"vtv1","name":"VTV1","desc":"Thời sự · Chính trị","yt_channel":"UCRLKY3loGMTmFLGO0BWQVeg","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv1.png"},
32
+ {"id":"vtv24","name":"VTV24","desc":"Tin tức 24h","yt_channel":"UCabsTV34JwALXKGMqHpvUiA","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv1.png"},
33
+ {"id":"vtv3","name":"VTV3","desc":"Giải trí · Thể thao","yt_channel":"UCMcbSjRHU5kw4nWnUz3CXyw","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv3.png"},
34
+ {"id":"vtvgo","name":"VTV Go","desc":"Tổng hợp VTV","yt_channel":"UCz2hUAOAOBfFTm7x0OTzxUQ","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv3.png"},
35
+ {"id":"thoisuvtv","name":"Thời Sự VTV","desc":"Chương trình Thời sự","yt_channel":"UCp4jnEEmOLEIg3bR3qCBwKg","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv1.png"},
36
+ {"id":"vtvthethao","name":"VTV Thể Thao","desc":"Bóng đá · Thể thao","yt_channel":"UCwa4OajMFekfVeHOS0F6bMg","logo":"https://vtvgo-images.vtvdigital.vn/images/vtv_channel/thumb/vtv6.png"},
 
 
37
  ]
38
 
39
+ @app.get("/api/livetv")
40
+ def api_livetv():
41
+ """Return list of live TV channels (YouTube-based)."""
42
+ return JSONResponse(VTV_CHANNELS)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
43
 
44
  def scrape_vne(cat_url):
45
  try:
 
275
  def api_shorts():return JSONResponse(_cached("shorts",scrape_24h_shorts))
276
  @app.get("/api/bdp_videos")
277
  def api_bdp_videos():return JSONResponse(_cached("bdp_videos",scrape_bdp_videos))
 
 
 
 
 
 
 
 
 
 
 
 
278
  @app.get("/api/video_url")
279
  def api_video_url(url:str=Query(...)):
280
  if"24h.com.vn" in url:v=extract_video_url(url)
 
289
  return JSONResponse(data if data else{"error":"not supported"})
290
  @app.get("/v")
291
  async def video_share(url:str=Query(default=""),title:str=Query(default="VNEWS Video"),img:str=Query(default=""),type:str=Query(default="highlights")):
 
292
  og_image=unquote(img) if img else "https://s1.vnecdn.net/vnexpress/restruct/i/v9505/logo_default.jpg"
293
  decoded_url=unquote(url)
294
  decoded_title=unquote(title)