bep40 commited on
Commit
fe96d1f
·
verified ·
1 Parent(s): d259953

Restore e77a637: vtv_shorts.py

Browse files
Files changed (1) hide show
  1. vtv_shorts.py +6 -160
vtv_shorts.py CHANGED
@@ -1,10 +1,5 @@
1
  """
2
  VTV Nam Bộ YouTube Shorts Scraper
3
- - Fetches channel ID from YouTube page
4
- - Uses YouTube RSS feed to get all videos
5
- - Filters for shorts (#shorts in title or /shorts/ in link)
6
- - Falls back to yt-dlp if RSS fails
7
- - Returns list of shorts with id, title, thumbnail
8
  """
9
  import requests
10
  import re
@@ -17,7 +12,7 @@ from xml.etree import ElementTree as ET
17
 
18
  _cache = {}
19
  _lock = threading.Lock()
20
- CACHE_TTL = 1800 # 30 min
21
 
22
  UA = {
23
  "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",
@@ -34,13 +29,10 @@ def _set_cache(key, data):
34
  with _lock:
35
  _cache[key] = {'t': time.time(), 'd': data}
36
 
37
-
38
  def get_channel_id(username):
39
- """Get YouTube channel ID from username by parsing the channel page."""
40
  cached = _cached(f'ch_id_{username}')
41
  if cached:
42
  return cached
43
-
44
  channel_id = None
45
  try:
46
  url = f"https://www.youtube.com/@{username}"
@@ -49,76 +41,49 @@ def get_channel_id(username):
49
  m = re.search(r'<meta\s+property="og:url"\s+content="https://www.youtube.com/channel/(UC[^"]+)"', r.text)
50
  if m:
51
  channel_id = m.group(1)
52
- if not channel_id:
53
- m = re.search(r'<link\s+rel="canonical"\s+href="https://www.youtube.com/channel/(UC[^"]+)"', r.text)
54
- if m:
55
- channel_id = m.group(1)
56
  if not channel_id:
57
  m = re.search(r'"channelId":"(UC[^"]+)"', r.text)
58
  if m:
59
  channel_id = m.group(1)
60
- if not channel_id:
61
- m = re.search(r'"externalId":"(UC[^"]+)"', r.text)
62
- if m:
63
- channel_id = m.group(1)
64
  except Exception as e:
65
  print(f"Error getting channel ID for @{username}: {e}")
66
-
67
  if channel_id:
68
  _set_cache(f'ch_id_{username}', channel_id)
69
  return channel_id
70
 
71
-
72
  def scrape_via_rss(channel_id, max_count=100):
73
- """Scrape videos from YouTube RSS feed."""
74
  shorts = []
75
  try:
76
  url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
77
  r = requests.get(url, headers=UA, timeout=15)
78
  if r.status_code != 200:
79
  return shorts
80
-
81
  root = ET.fromstring(r.text)
82
  ns = {'atom': 'http://www.w3.org/2005/Atom', 'yt': 'http://www.youtube.com/xml/schemas/2015'}
83
-
84
  for entry in root.findall('atom:entry', ns)[:max_count]:
85
  title_el = entry.find('atom:title', ns)
86
  title = html_lib.unescape(title_el.text) if title_el is not None and title_el.text else ''
87
-
88
  vid_el = entry.find('yt:videoId', ns)
89
  vid = vid_el.text if vid_el is not None else ''
90
  if not vid:
91
  continue
92
-
93
  is_short = '#shorts' in title.lower() or '#short' in title.lower()
94
-
95
  link_el = entry.find('atom:link', ns)
96
  link = link_el.get('href', '') if link_el is not None else ''
97
  if '/shorts/' in link:
98
  is_short = True
99
-
100
  if is_short:
101
- shorts.append({
102
- 'id': vid,
103
- 'title': title,
104
- 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
105
- 'channel': 'vtvnambo',
106
- })
107
  except Exception as e:
108
  print(f"RSS scrape error: {e}")
109
-
110
  return shorts
111
 
112
-
113
  def scrape_via_ydlp(username, count=50):
114
- """Scrape shorts using yt-dlp."""
115
  shorts = []
116
  try:
117
  url = f"https://www.youtube.com/@{username}/shorts"
118
  result = subprocess.run(
119
- ["yt-dlp", "--dump-json", "--flat-playlist", "--no-download",
120
- "--playlist-end", str(count),
121
- url],
122
  capture_output=True, text=True, timeout=90
123
  )
124
  if result.returncode == 0 and result.stdout.strip():
@@ -134,71 +99,19 @@ def scrape_via_ydlp(username, count=50):
134
  continue
135
  seen_ids.add(vid)
136
  title = entry.get('title', 'VTV Nam Bộ Short')
137
- shorts.append({
138
- 'id': vid,
139
- 'title': title,
140
- 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
141
- 'channel': 'vtvnambo',
142
- })
143
  except json.JSONDecodeError:
144
  continue
145
  except (subprocess.TimeoutExpired, FileNotFoundError) as e:
146
  print(f"yt-dlp error: {e}")
147
- except Exception as e:
148
- print(f"yt-dlp error: {e}")
149
-
150
- return shorts
151
-
152
-
153
- def scrape_via_ydlp_videos_filter(username, count=100):
154
- """Scrape all videos and filter for shorts by duration."""
155
- shorts = []
156
- try:
157
- url = f"https://www.youtube.com/@{username}/videos"
158
- result = subprocess.run(
159
- ["yt-dlp", "--dump-json", "--flat-playlist", "--no-download",
160
- "--playlist-end", str(count),
161
- "--match-filter", "duration < 65",
162
- url],
163
- capture_output=True, text=True, timeout=90
164
- )
165
- if result.returncode == 0 and result.stdout.strip():
166
- seen_ids = set()
167
- for line in result.stdout.strip().split('\n'):
168
- line = line.strip()
169
- if not line:
170
- continue
171
- try:
172
- entry = json.loads(line)
173
- vid = entry.get('id', '')
174
- if not vid or vid in seen_ids:
175
- continue
176
- seen_ids.add(vid)
177
- title = entry.get('title', 'VTV Nam Bộ Short')
178
- shorts.append({
179
- 'id': vid,
180
- 'title': title,
181
- 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
182
- 'channel': 'vtvnambo',
183
- })
184
- except json.JSONDecodeError:
185
- continue
186
- except Exception as e:
187
- print(f"yt-dlp videos filter error: {e}")
188
-
189
  return shorts
190
 
191
-
192
  def get_vtvnambo_shorts(max_count=50):
193
- """Get all shorts from VTV Nam Bộ channel. Tries multiple methods."""
194
  cached = _cached('vtvnambo_shorts_v2')
195
  if cached is not None:
196
  return cached
197
-
198
  all_shorts = []
199
  seen_ids = set()
200
-
201
- # Method 1: RSS feed (most reliable, no JS needed)
202
  channel_id = get_channel_id('vtvnambo')
203
  if channel_id:
204
  rss_shorts = scrape_via_rss(channel_id, max_count * 2)
@@ -206,89 +119,22 @@ def get_vtvnambo_shorts(max_count=50):
206
  if s['id'] not in seen_ids:
207
  seen_ids.add(s['id'])
208
  all_shorts.append(s)
209
-
210
- # Method 2: yt-dlp on shorts page
211
  if len(all_shorts) < 3:
212
  ydlp_shorts = scrape_via_ydlp('vtvnambo', max_count)
213
  for s in ydlp_shorts:
214
  if s['id'] not in seen_ids:
215
  seen_ids.add(s['id'])
216
  all_shorts.append(s)
217
-
218
- # Method 3: yt-dlp on videos page with duration filter
219
- if len(all_shorts) < 3:
220
- ydlp_vids = scrape_via_ydlp_videos_filter('vtvnambo', max_count * 2)
221
- for s in ydlp_vids:
222
- if s['id'] not in seen_ids:
223
- seen_ids.add(s['id'])
224
- all_shorts.append(s)
225
-
226
- # Method 4: requests + regex on shorts page
227
- if len(all_shorts) < 3:
228
- try:
229
- url = "https://www.youtube.com/@vtvnambo/shorts"
230
- r = requests.get(url, headers=UA, timeout=15)
231
- if r.status_code == 200:
232
- for m in re.finditer(r'"videoId":"([A-Za-z0-9_-]{11})"', r.text):
233
- vid = m.group(1)
234
- if vid in seen_ids:
235
- continue
236
- seen_ids.add(vid)
237
- snip = r.text[max(0, m.start() - 900):m.start() + 1600]
238
- title = ""
239
- mt = re.search(r'"title":\{"runs":\[\{"text":"([^"]+)"', snip)
240
- if not mt:
241
- mt = re.search(r'"accessibilityText":"([^"]+)"', snip)
242
- if mt:
243
- title = html_lib.unescape(mt.group(1)).replace('\n', ' ').strip()
244
- if not title:
245
- title = "VTV Nam Bộ Short"
246
- all_shorts.append({
247
- 'id': vid,
248
- 'title': title,
249
- 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
250
- 'channel': 'vtvnambo',
251
- })
252
- except Exception as e:
253
- print(f"Requests regex error: {e}")
254
-
255
  result = all_shorts[:max_count]
256
  _set_cache('vtvnambo_shorts_v2', result)
257
  return result
258
 
259
-
260
- # Alias for backward compatibility (fix the typo import in main.py)
261
  get_vtvnamo_shorts = get_vtvnambo_shorts
262
 
263
-
264
  def get_wc_related_shorts(max_count=30):
265
- """Get World Cup / football related shorts from VTV Nam Bộ."""
266
  all_shorts = get_vtvnambo_shorts(max_count * 3)
267
-
268
- wc_kws = [
269
- 'world cup', 'wc 2026', 'worldcup', 'fifa', 'bóng đá',
270
- 'trận đấu', 'đội tuyển', 'tuyển', 'vòng loại',
271
- 'khoảnh khắc', 'highlights', 'bàn thắng', 'goal',
272
- 'kết quả', 'tỉ số', 'việt nam', 'vn',
273
- 'ngoại hạng', 'premier league', 'champions league',
274
- 'laliga', 'serie a', 'bundesliga', 'ligue 1',
275
- 'copa', 'europa', 'c1', 'c2',
276
- 'messi', 'ronaldo', 'neymar', 'mbappe', 'haaland',
277
- 'v-league', 'vleague', 'bóng đá việt',
278
- 'đội bóng', 'hlv', 'huấn luyện viên',
279
- 'chuyển nhượng', 'transfer',
280
- 'asian cup', 'aff cup', 'sea games',
281
- 'olympic', 'u23', 'u20', 'u17',
282
- ]
283
-
284
- wc_shorts = []
285
- for s in all_shorts:
286
- tl = s.get('title', '').lower()
287
- if any(k in tl for k in wc_kws):
288
- wc_shorts.append(s)
289
-
290
- # If no WC-specific shorts found, return all VTV shorts
291
  if not wc_shorts:
292
  wc_shorts = all_shorts
293
-
294
  return wc_shorts[:max_count]
 
1
  """
2
  VTV Nam Bộ YouTube Shorts Scraper
 
 
 
 
 
3
  """
4
  import requests
5
  import re
 
12
 
13
  _cache = {}
14
  _lock = threading.Lock()
15
+ CACHE_TTL = 1800
16
 
17
  UA = {
18
  "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",
 
29
  with _lock:
30
  _cache[key] = {'t': time.time(), 'd': data}
31
 
 
32
  def get_channel_id(username):
 
33
  cached = _cached(f'ch_id_{username}')
34
  if cached:
35
  return cached
 
36
  channel_id = None
37
  try:
38
  url = f"https://www.youtube.com/@{username}"
 
41
  m = re.search(r'<meta\s+property="og:url"\s+content="https://www.youtube.com/channel/(UC[^"]+)"', r.text)
42
  if m:
43
  channel_id = m.group(1)
 
 
 
 
44
  if not channel_id:
45
  m = re.search(r'"channelId":"(UC[^"]+)"', r.text)
46
  if m:
47
  channel_id = m.group(1)
 
 
 
 
48
  except Exception as e:
49
  print(f"Error getting channel ID for @{username}: {e}")
 
50
  if channel_id:
51
  _set_cache(f'ch_id_{username}', channel_id)
52
  return channel_id
53
 
 
54
  def scrape_via_rss(channel_id, max_count=100):
 
55
  shorts = []
56
  try:
57
  url = f"https://www.youtube.com/feeds/videos.xml?channel_id={channel_id}"
58
  r = requests.get(url, headers=UA, timeout=15)
59
  if r.status_code != 200:
60
  return shorts
 
61
  root = ET.fromstring(r.text)
62
  ns = {'atom': 'http://www.w3.org/2005/Atom', 'yt': 'http://www.youtube.com/xml/schemas/2015'}
 
63
  for entry in root.findall('atom:entry', ns)[:max_count]:
64
  title_el = entry.find('atom:title', ns)
65
  title = html_lib.unescape(title_el.text) if title_el is not None and title_el.text else ''
 
66
  vid_el = entry.find('yt:videoId', ns)
67
  vid = vid_el.text if vid_el is not None else ''
68
  if not vid:
69
  continue
 
70
  is_short = '#shorts' in title.lower() or '#short' in title.lower()
 
71
  link_el = entry.find('atom:link', ns)
72
  link = link_el.get('href', '') if link_el is not None else ''
73
  if '/shorts/' in link:
74
  is_short = True
 
75
  if is_short:
76
+ shorts.append({'id': vid, 'title': title, 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg", 'channel': 'vtvnambo'})
 
 
 
 
 
77
  except Exception as e:
78
  print(f"RSS scrape error: {e}")
 
79
  return shorts
80
 
 
81
  def scrape_via_ydlp(username, count=50):
 
82
  shorts = []
83
  try:
84
  url = f"https://www.youtube.com/@{username}/shorts"
85
  result = subprocess.run(
86
+ ["yt-dlp", "--dump-json", "--flat-playlist", "--no-download", "--playlist-end", str(count), url],
 
 
87
  capture_output=True, text=True, timeout=90
88
  )
89
  if result.returncode == 0 and result.stdout.strip():
 
99
  continue
100
  seen_ids.add(vid)
101
  title = entry.get('title', 'VTV Nam Bộ Short')
102
+ shorts.append({'id': vid, 'title': title, 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg", 'channel': 'vtvnambo'})
 
 
 
 
 
103
  except json.JSONDecodeError:
104
  continue
105
  except (subprocess.TimeoutExpired, FileNotFoundError) as e:
106
  print(f"yt-dlp error: {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
107
  return shorts
108
 
 
109
  def get_vtvnambo_shorts(max_count=50):
 
110
  cached = _cached('vtvnambo_shorts_v2')
111
  if cached is not None:
112
  return cached
 
113
  all_shorts = []
114
  seen_ids = set()
 
 
115
  channel_id = get_channel_id('vtvnambo')
116
  if channel_id:
117
  rss_shorts = scrape_via_rss(channel_id, max_count * 2)
 
119
  if s['id'] not in seen_ids:
120
  seen_ids.add(s['id'])
121
  all_shorts.append(s)
 
 
122
  if len(all_shorts) < 3:
123
  ydlp_shorts = scrape_via_ydlp('vtvnambo', max_count)
124
  for s in ydlp_shorts:
125
  if s['id'] not in seen_ids:
126
  seen_ids.add(s['id'])
127
  all_shorts.append(s)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
  result = all_shorts[:max_count]
129
  _set_cache('vtvnambo_shorts_v2', result)
130
  return result
131
 
 
 
132
  get_vtvnamo_shorts = get_vtvnambo_shorts
133
 
 
134
  def get_wc_related_shorts(max_count=30):
 
135
  all_shorts = get_vtvnambo_shorts(max_count * 3)
136
+ wc_kws = ['world cup', 'wc 2026', 'worldcup', 'fifa', 'bóng đá', 'trận đấu', 'đội tuyển']
137
+ wc_shorts = [s for s in all_shorts if any(k in s.get('title', '').lower() for k in wc_kws)]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
138
  if not wc_shorts:
139
  wc_shorts = all_shorts
 
140
  return wc_shorts[:max_count]