bep40 commited on
Commit
ecfd22b
·
verified ·
1 Parent(s): fa52ede

Upload vtv_scraper.py

Browse files
Files changed (1) hide show
  1. vtv_scraper.py +224 -0
vtv_scraper.py ADDED
@@ -0,0 +1,224 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ VTV Channel Scraper - Fetches live stream URLs from xemtv.net
3
+ Scrapes VTV1-VTV10 + VTV Cần Thơ stream URLs dynamically.
4
+ """
5
+ import requests, re, json, time, threading
6
+ from bs4 import BeautifulSoup
7
+ from urllib.parse import urljoin
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",
11
+ "Accept-Language": "vi-VN,vi;q=0.9,en;q=0.8",
12
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
13
+ }
14
+ REFERER = "https://hd.xemtv.net/"
15
+
16
+ # Known xemtv.net channel page patterns
17
+ XEMTV_CHANNELS = {
18
+ "VTV1": "https://hd.xemtv.net/kenh-vtv1-truc-tuyen-hot.html",
19
+ "VTV2": "https://hd.xemtv.net/kenh-vtv2-truc-tuyen-hot.html",
20
+ "VTV3": "https://hd.xemtv.net/kenh-vtv3-truc-tuyen-hot.html",
21
+ "VTV4": "https://hd.xemtv.net/kenh-vtv4-truc-tuyen-hot.html",
22
+ "VTV5": "https://hd.xemtv.net/kenh-vtv5-truc-tuyen-hot.html",
23
+ "VTV6": "https://hd.xemtv.net/kenh-vtv-can-tho-truc-tuyen-hot.html", # VTV Cần Thơ
24
+ "VTV7": "https://hd.xemtv.net/kenh-vtv7-truc-tuyen-hot.html",
25
+ "VTV8": "https://hd.xemtv.net/kenh-vtv8-truc-tuyen-hot.html",
26
+ "VTV9": "https://hd.xemtv.net/kenh-vtv9-truc-tuyen-hot.html",
27
+ "VTV10": "https://hd.xemtv.net/kenh-vtv10-truc-tuyen-hot.html",
28
+ }
29
+
30
+ # Fallback: try alternate URL patterns
31
+ XEMTV_ALT_PATTERNS = [
32
+ "https://hd.xemtv.net/kenh-{slug}-truc-tuyen-hot.html",
33
+ "https://hd.xemtv.net/{slug}-truc-tuyen-hot.html",
34
+ "https://xemtv.net/kenh-{slug}-truc-tuyen-hot.html",
35
+ ]
36
+
37
+ _vtv_cache = {}
38
+ _vtv_cache_lock = threading.Lock()
39
+ _cache_ttl = 600 # 10 minutes
40
+
41
+
42
+ def _cached(key):
43
+ with _vtv_cache_lock:
44
+ if key in _vtv_cache and time.time() - _vtv_cache[key]['t'] < _cache_ttl:
45
+ return _vtv_cache[key]['d']
46
+ return None
47
+
48
+
49
+ def _set_cache(key, data):
50
+ with _vtv_cache_lock:
51
+ _vtv_cache[key] = {'t': time.time(), 'd': data}
52
+
53
+
54
+ def _extract_stream_from_page(html, page_url=""):
55
+ """Extract m3u8/stream URL from xemtv.net page HTML."""
56
+ streams = []
57
+
58
+ # Pattern 1: direct m3u8 URLs in page source
59
+ for m in re.finditer(r'(https?://[^\s"\'<>]+\.m3u8[^\s"\'<>]*)', html):
60
+ url = m.group(1).strip()
61
+ if url not in streams:
62
+ streams.append(url)
63
+
64
+ # Pattern 2: m3u8 in JavaScript variables
65
+ for m in re.finditer(r'(?:src|source|url|file|stream|play_url|video_url)\s*[:=]\s*["\']([^"\']*\.m3u8[^"\']*)["\']', html, re.I):
66
+ url = m.group(1).strip()
67
+ if url not in streams:
68
+ streams.append(url)
69
+
70
+ # Pattern 3: in JSON-like config blocks
71
+ for m in re.finditer(r'"(?:src|source|url|file|stream|play_url|video_url|m3u8)"\s*:\s*"(https?://[^"]*\.m3u8[^"]*)"', html, re.I):
72
+ url = m.group(1).strip()
73
+ if url not in streams:
74
+ streams.append(url)
75
+
76
+ # Pattern 4: iframe src that might contain stream
77
+ for m in re.finditer(r'<iframe[^>]+src=["\']([^"\']+)["\']', html, re.I):
78
+ iframe_url = m.group(1).strip()
79
+ if 'xemtv' in iframe_url or 'embed' in iframe_url.lower():
80
+ streams.append(("iframe", iframe_url))
81
+
82
+ # Pattern 5: video/source tags
83
+ soup = BeautifulSoup(html, 'lxml')
84
+ for video in soup.find_all('video'):
85
+ src = video.get('src', '')
86
+ if src and '.m3u8' in src:
87
+ streams.append(src)
88
+ for source in video.find_all('source'):
89
+ src = source.get('src', '')
90
+ if src and '.m3u8' in src and src not in streams:
91
+ streams.append(src)
92
+
93
+ # Pattern 6: data-* attributes
94
+ for tag in soup.find_all(attrs={"data-url": True}):
95
+ url = tag['data-url']
96
+ if '.m3u8' in url and url not in streams:
97
+ streams.append(url)
98
+ for tag in soup.find_all(attrs={"data-src": True}):
99
+ url = tag['data-src']
100
+ if '.m3u8' in url and url not in streams:
101
+ streams.append(url)
102
+ for tag in soup.find_all(attrs={"data-stream": True}):
103
+ url = tag['data-stream']
104
+ if '.m3u8' in url and url not in streams:
105
+ streams.append(url)
106
+
107
+ # Filter: only keep direct m3u8 URLs (not iframe tuples)
108
+ direct = [s for s in streams if isinstance(s, str) and s.startswith('http')]
109
+ return direct
110
+
111
+
112
+ def _extract_stream_via_jina(page_url):
113
+ """Use Jina AI to fetch page and extract stream URLs."""
114
+ try:
115
+ r = requests.get(
116
+ "https://r.jina.ai/" + page_url,
117
+ headers={"Accept": "text/markdown", "X-Return-Format": "markdown", "User-Agent": "Mozilla/5.0"},
118
+ timeout=30
119
+ )
120
+ if r.status_code == 200:
121
+ # Jina returns markdown, but we need the raw HTML for stream extraction
122
+ # Try to find m3u8 in the markdown output
123
+ for m in re.finditer(r'(https?://[^\s<>)]+\.m3u8[^\s<>)]*)', r.text):
124
+ return [m.group(1)]
125
+ except:
126
+ pass
127
+ return []
128
+
129
+
130
+ def fetch_vtv_stream(channel_name):
131
+ """Fetch stream URL for a specific VTV channel from xemtv.net."""
132
+ cache_key = f"vtv_{channel_name}"
133
+ cached = _cached(cache_key)
134
+ if cached:
135
+ return cached
136
+
137
+ url = XEMTV_CHANNELS.get(channel_name)
138
+ if not url:
139
+ return None
140
+
141
+ streams = []
142
+
143
+ # Method 1: Direct fetch
144
+ try:
145
+ r = requests.get(url, headers={**UA, "Referer": REFERER}, timeout=15, allow_redirects=True)
146
+ if r.status_code == 200:
147
+ r.encoding = 'utf-8'
148
+ streams = _extract_stream_from_page(r.text, url)
149
+ except:
150
+ pass
151
+
152
+ # Method 2: Jina AI
153
+ if not streams:
154
+ streams = _extract_stream_via_jia(url)
155
+
156
+ # Method 3: Try alternate URL patterns
157
+ if not streams:
158
+ slug = channel_name.lower().replace(' ', '-')
159
+ for pattern in XEMTV_ALT_PATTERNS:
160
+ alt_url = pattern.format(slug=slug)
161
+ try:
162
+ r = requests.get(alt_url, headers={**UA, "Referer": REFERER}, timeout=10, allow_redirects=True)
163
+ if r.status_code == 200:
164
+ r.encoding = 'utf-8'
165
+ streams = _extract_stream_from_page(r.text, alt_url)
166
+ if streams:
167
+ break
168
+ except:
169
+ pass
170
+
171
+ result = streams[0] if streams else None
172
+ _set_cache(cache_key, result)
173
+ return result
174
+
175
+
176
+ def fetch_all_vtv_streams():
177
+ """Fetch all VTV channel streams. Returns {channel_name: stream_url}."""
178
+ cache_key = "vtv_all"
179
+ cached = _cached(cache_key)
180
+ if cached:
181
+ return cached
182
+
183
+ from concurrent.futures import ThreadPoolExecutor, as_completed
184
+
185
+ results = {}
186
+
187
+ def _fetch_one(name):
188
+ url = fetch_vtv_stream(name)
189
+ return name, url
190
+
191
+ with ThreadPoolExecutor(5) as ex:
192
+ futs = [ex.submit(_fetch_one, name) for name in XEMTV_CHANNELS]
193
+ for f in as_completed(futs, timeout=30):
194
+ try:
195
+ name, url = f.result()
196
+ if url:
197
+ results[name] = url
198
+ except:
199
+ pass
200
+
201
+ _set_cache(cache_key, results)
202
+ return results
203
+
204
+
205
+ def get_vtv_channels_for_frontend():
206
+ """
207
+ Returns channel data for the frontend.
208
+ Tries xemtv.net first, falls back to known working streams.
209
+ """
210
+ streams = fetch_all_vtv_streams()
211
+
212
+ # Build ordered channel list
213
+ channels = []
214
+ for name in ["VTV1", "VTV2", "VTV3", "VTV4", "VTV5", "VTV6", "VTV7", "VTV8", "VTV9", "VTV10"]:
215
+ url = streams.get(name)
216
+ display_name = name if name != "VTV6" else "VTV Cần Thơ"
217
+ channels.append({
218
+ "name": name,
219
+ "displayName": display_name,
220
+ "url": url or "",
221
+ "logo": f"https://upload.wikimedia.org/wikipedia/commons/thumb/4/4f/{name}_logo.svg/200px-{name}_logo.svg.png"
222
+ })
223
+
224
+ return channels