bep40 commited on
Commit
fff0b04
·
verified ·
1 Parent(s): c0c2360

Fix VTV scraper: add VTV6/VTV10, scrape xemtv PHP for fresh URLs

Browse files
Files changed (1) hide show
  1. vtv_scraper.py +91 -75
vtv_scraper.py CHANGED
@@ -1,10 +1,9 @@
1
  """
2
  VTV Channels Scraper
3
- Fetches stream URLs from hd.xemtv.net for VTV1-VTV9 + VTV Cần Thơ
4
- Uses fptplay CDN as primary source (accessed via backend proxy with Referer header)
5
  """
6
  import requests, re, time, threading
7
- from bs4 import BeautifulSoup
8
 
9
  UA = {
10
  "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",
@@ -12,36 +11,53 @@ UA = {
12
  "Referer": "https://hd.xemtv.net/",
13
  }
14
 
15
- # Channel page URLs on xemtv.net (for scraping)
16
- XEMTV_CHANNELS = {
17
- "VTV1": "https://hd.xemtv.net/kenh-vtv1-truc-tuyen-hot.html",
18
- "VTV2": "https://hd.xemtv.net/kenh-vtv2-truc-tuyen-hot.html",
19
- "VTV3": "https://hd.xemtv.net/kenh-vtv3-truc-tuyen-hot.html",
20
- "VTV4": "https://hd.xemtv.net/kenh-vtv4-truc-tuyen-hot.html",
21
- "VTV5": "https://hd.xemtv.net/kenh-vtv5-truc-tuyen-hot.html",
22
- "VTV CẦN THƠ": "https://hd.xemtv.net/kenh-vtv10-truc-tuyen-hot.html",
23
- "VTV7": "https://hd.xemtv.net/kenh-vtv7-truc-tuyen-hot.html",
24
- "VTV8": "https://hd.xemtv.net/kenh-vtv8-truc-tuyen-hot.html",
25
- "VTV9": "https://hd.xemtv.net/kenh-vtv9-truc-tuyen-hot.html",
 
 
26
  }
27
 
28
- # Primary CDN streams (fptplay) — these require Referer: https://fptplay.vn/
29
- # The backend proxy (/api/proxy/hls) adds this header
30
- CDN_STREAMS = {
31
- "VTV1": "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
32
- "VTV2": "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
33
- "VTV3": "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
34
- "VTV4": "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
35
- "VTV5": "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8",
36
- "VTV CẦN THƠ": "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
37
- "VTV7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
38
- "VTV8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
39
- "VTV9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  }
41
 
42
  _vtv_cache = {}
43
  _vtv_lock = threading.Lock()
44
- _CACHE_TTL = 300 # 5 minutes
45
 
46
 
47
  def _cached(key):
@@ -57,84 +73,84 @@ def _set_cache(key, data):
57
 
58
 
59
  def extract_m3u8_from_html(html):
60
- """Extract m3u8 URL from page HTML."""
61
  if not html:
62
  return None
63
- patterns = [
64
- r'(https?://[^"\'<>\s]+\.m3u8[^"\'<>\s]*)',
65
- r'file\s*[:=]\s*["\']([^"\']*\.m3u8[^"\']*)',
66
- r'source\s*[:=]\s*["\']([^"\']*\.m3u8[^"\']*)',
67
- r'src\s*[:=]\s*["\']([^"\']*\.m3u8[^"\']*)',
68
- r'url\s*[:=]\s*["\']([^"\']*\.m3u8[^"\']*)',
69
- r'hls\.loadSource\(["\']([^"\']*\.m3u8[^"\']*)',
70
- r'player\.src\s*=\s*["\']([^"\']*\.m3u8[^"\']*)',
71
- r'"file"\s*:\s*"([^"]*\.m3u8[^"]*)',
72
- r'"src"\s*:\s*"([^"]*\.m3u8[^"]*)',
73
- r'"url"\s*:\s*"([^"]*\.m3u8[^"]*)',
74
- ]
75
- for pat in patterns:
76
- m = re.search(pat, html, re.IGNORECASE)
77
- if m:
78
- url = m.group(1).strip()
79
- if '.m3u8' in url and len(url) > 20:
80
- return url
81
  return None
82
 
83
 
84
- def fetch_vtv_stream(channel_name):
85
- """Fetch m3u8 stream URL for a VTV channel from xemtv.net."""
86
- channel_name = channel_name.upper().strip()
87
 
88
  # Normalize name
89
  name_map = {
90
- 'VTVCT': 'VTV CẦN THƠ', 'VTV-CAN-THO': 'VTV CẦN THƠ',
91
- 'VTV CAN THO': 'VTV CẦN THƠ', 'VTV6': 'VTV CẦN THƠ',
92
- 'VTCT': 'VTV CẦN THƠ',
 
 
93
  }
94
- channel_name = name_map.get(channel_name, channel_name)
95
 
96
  # Check cache
97
- cached = _cached(channel_name)
98
  if cached:
99
  return cached
100
 
101
- page_url = XEMTV_CHANNELS.get(channel_name)
102
- if not page_url:
103
- cdn = CDN_STREAMS.get(channel_name)
104
- if cdn:
105
- _set_cache(channel_name, cdn)
106
- return cdn
 
107
  return None
108
 
109
  try:
110
- r = requests.get(page_url, headers=UA, timeout=15, allow_redirects=True)
111
  if r.status_code == 200:
112
  m3u8 = extract_m3u8_from_html(r.text)
113
  if m3u8:
114
- _set_cache(channel_name, m3u8)
115
  return m3u8
116
  except:
117
  pass
118
 
119
  # Fallback to CDN
120
- cdn = CDN_STREAMS.get(channel_name)
121
- if cdn:
122
- _set_cache(channel_name, cdn)
123
- return cdn
124
 
125
  return None
126
 
127
 
128
- def get_vtv_channels_for_frontend():
129
- """Get all VTV channels with their stream URLs for the frontend."""
130
  channels = []
131
- for name, page_url in XEMTV_CHANNELS.items():
132
- stream_url = fetch_vtv_stream(name)
133
- cdn_url = CDN_STREAMS.get(name, '')
134
  channels.append({
135
- 'name': name,
136
- 'page_url': page_url,
137
- 'stream_url': stream_url or cdn_url,
138
- 'cdn_fallback': cdn_url,
139
  })
140
  return channels
 
 
 
 
 
 
1
  """
2
  VTV Channels Scraper
3
+ Fetches stream URLs from hd.xemtv.net PHP endpoints for VTV1-VTV10 + VTV Cần Thơ
4
+ The PHP endpoints return jwplayer config with fresh stream URLs (important for VTV6 which has expiring signatures)
5
  """
6
  import requests, re, time, threading
 
7
 
8
  UA = {
9
  "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",
 
11
  "Referer": "https://hd.xemtv.net/",
12
  }
13
 
14
+ # Channel ID -> xemtv.net PHP endpoint mapping
15
+ # These PHP pages return jwplayer config with fresh stream URLs
16
+ XEMTV_PHP_ENDPOINTS = {
17
+ "vtv1": "https://hd.xemtv.net/kenh/vtv1.php",
18
+ "vtv2": "https://hd.xemtv.net/kenh/vtv2.php",
19
+ "vtv3": "https://hd.xemtv.net/kenh/vtv3.php",
20
+ "vtv4": "https://hd.xemtv.net/kenh/vtv4.php",
21
+ "vtv5": "https://hd.xemtv.net/kenh/vtv5.php",
22
+ "vtv6": "https://hd.xemtv.net/kenh/vtv6.php",
23
+ "vtv7": "https://hd.xemtv.net/kenh/vtv7.php",
24
+ "vtv8": "https://hd.xemtv.net/kenh/vtv8.php",
25
+ "vtv9": "https://hd.xemtv.net/kenh/vtv9.php",
26
+ "vtv10": "https://hd.xemtv.net/kenh/vtv10.php", # VTV Cần Thơ
27
  }
28
 
29
+ # Channel display names
30
+ CHANNEL_NAMES = {
31
+ "vtv1": "VTV1",
32
+ "vtv2": "VTV2",
33
+ "vtv3": "VTV3",
34
+ "vtv4": "VTV4",
35
+ "vtv5": "VTV5",
36
+ "vtv6": "VTV6",
37
+ "vtv7": "VTV7",
38
+ "vtv8": "VTV8",
39
+ "vtv9": "VTV9",
40
+ "vtv10": "VTV Cần Thơ",
41
+ }
42
+
43
+ # Fallback CDN streams (fptplay) — used when xemtv scraping fails
44
+ CDN_FALLBACK = {
45
+ "vtv1": "https://live.fptplay53.net/fnxch2/vtv1hd_abr.smil/chunklist.m3u8",
46
+ "vtv2": "https://live.fptplay53.net/fnxch2/vtv2hd_abr.smil/chunklist.m3u8",
47
+ "vtv3": "https://live.fptplay53.net/fnxch2/vtv3hd_abr.smil/chunklist.m3u8",
48
+ "vtv4": "https://live.fptplay53.net/fnxch2/vtv4hd_abr.smil/chunklist.m3u8",
49
+ "vtv5": "https://live-a.fptplay53.net/live/media/VTV5HD/live_hls_avc/index.m3u8",
50
+ "vtv7": "https://live.fptplay53.net/fnxhd1/vtv7hd_vhls.smil/chunklist_b5000000.m3u8",
51
+ "vtv8": "https://live.fptplay53.net/epzhd1/vtv8hd_vhls.smil/chunklist.m3u8",
52
+ "vtv9": "https://live.fptplay53.net/fnxhd1/vtv9hd_vhls.smil/chunklist.m3u8",
53
+ "vtv10": "https://live.fptplay53.net/fnxch2/vtvcantho_abr.smil/chunklist.m3u8",
54
+ # VTV6 fallback: try canthotv as alternative
55
+ "vtv6": "https://live.canthotv.vn/live/tv/chunklist.m3u8",
56
  }
57
 
58
  _vtv_cache = {}
59
  _vtv_lock = threading.Lock()
60
+ _CACHE_TTL = 180 # 3 minutes — refresh frequently for VTV6 sign URLs
61
 
62
 
63
  def _cached(key):
 
73
 
74
 
75
  def extract_m3u8_from_html(html):
76
+ """Extract m3u8 URL from xemtv PHP page (jwplayer config)."""
77
  if not html:
78
  return None
79
+ # jwplayer config: file: 'URL'
80
+ m = re.search(r"file\s*:\s*['\"]([^'\"]*\.m3u8[^'\"]*)['\"]", html, re.IGNORECASE)
81
+ if m:
82
+ url = m.group(1).strip()
83
+ if len(url) > 20:
84
+ return url
85
+ # Generic m3u8 pattern
86
+ m = re.search(r"(https?://[^\s\"'<>\\]+\.m3u8[^\s\"'<>\\]*)", html, re.IGNORECASE)
87
+ if m:
88
+ url = m.group(1).strip()
89
+ if len(url) > 20:
90
+ return url
 
 
 
 
 
 
91
  return None
92
 
93
 
94
+ def fetch_vtv_stream(channel_id):
95
+ """Fetch m3u8 stream URL for a VTV channel by scraping xemtv.net PHP endpoint."""
96
+ channel_id = channel_id.lower().strip()
97
 
98
  # Normalize name
99
  name_map = {
100
+ 'vtvct': 'vtv10', 'vtv-can-tho': 'vtv10', 'vtv can tho': 'vtv10',
101
+ 'vtv_can_tho': 'vtv10', 'cantho': 'vtv10', 'cần thơ': 'vtv10',
102
+ 'vietnam_vtv1': 'vtv1', 'vietnam_vtv2': 'vtv2', 'vietnam_vtv3': 'vtv3',
103
+ 'vietnam_vtv4': 'vtv4', 'vietnam_vtv5': 'vtv5', 'vietnam_vtv6': 'vtv6',
104
+ 'vietnam_vtv7': 'vtv7', 'vietnam_vtv8': 'vtv8', 'vietnam_vtv9': 'vtv9',
105
  }
106
+ channel_id = name_map.get(channel_id, channel_id)
107
 
108
  # Check cache
109
+ cached = _cached(channel_id)
110
  if cached:
111
  return cached
112
 
113
+ php_url = XEMTV_PHP_ENDPOINTS.get(channel_id)
114
+ if not php_url:
115
+ # Fallback
116
+ fallback = CDN_FALLBACK.get(channel_id)
117
+ if fallback:
118
+ _set_cache(channel_id, fallback)
119
+ return fallback
120
  return None
121
 
122
  try:
123
+ r = requests.get(php_url, headers=UA, timeout=15, allow_redirects=True)
124
  if r.status_code == 200:
125
  m3u8 = extract_m3u8_from_html(r.text)
126
  if m3u8:
127
+ _set_cache(channel_id, m3u8)
128
  return m3u8
129
  except:
130
  pass
131
 
132
  # Fallback to CDN
133
+ fallback = CDN_FALLBACK.get(channel_id)
134
+ if fallback:
135
+ _set_cache(channel_id, fallback)
136
+ return fallback
137
 
138
  return None
139
 
140
 
141
+ def get_all_vtv_streams():
142
+ """Fetch all VTV channel streams. Returns list of {id, name, stream_url}."""
143
  channels = []
144
+ for ch_id, php_url in XEMTV_PHP_ENDPOINTS.items():
145
+ stream_url = fetch_vtv_stream(ch_id)
 
146
  channels.append({
147
+ 'id': ch_id,
148
+ 'name': CHANNEL_NAMES.get(ch_id, ch_id.upper()),
149
+ 'stream_url': stream_url,
 
150
  })
151
  return channels
152
+
153
+
154
+ # Legacy compatibility
155
+ XEMTV_CHANNELS = {v: k for k, v in CHANNEL_NAMES.items()}
156
+ CDN_STREAMS = {v: k for k, v in CDN_FALLBACK.items()}