bep40 commited on
Commit
a478c48
·
verified ·
1 Parent(s): 18467f5

Upload yt_scraper.py

Browse files
Files changed (1) hide show
  1. yt_scraper.py +235 -0
yt_scraper.py ADDED
@@ -0,0 +1,235 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ YouTube Shorts Scraper using yt-dlp (already installed on Space)
3
+ Runs yt-dlp as subprocess to extract video info and direct URLs
4
+ """
5
+ import subprocess
6
+ import json
7
+ import time
8
+ import threading
9
+ import os
10
+ import re as re_mod
11
+
12
+ _cache = {}
13
+ _lock = threading.Lock()
14
+ CACHE_TTL = 600 # 10 min cache
15
+
16
+ def _cached(key):
17
+ with _lock:
18
+ if key in _cache and time.time() - _cache[key]['t'] < CACHE_TTL:
19
+ return _cache[key]['d']
20
+ return None
21
+
22
+ def _set_cache(key, data):
23
+ with _lock:
24
+ _cache[key] = {'t': time.time(), 'd': data}
25
+
26
+ def run_yt_dlp(args, timeout=120):
27
+ """Run yt-dlp and return parsed JSON lines"""
28
+ try:
29
+ result = subprocess.run(
30
+ ["yt-dlp"] + args,
31
+ capture_output=True, text=True, timeout=timeout
32
+ )
33
+ if result.returncode != 0 and not result.stdout.strip():
34
+ print(f"yt-dlp error: {result.stderr[:200]}")
35
+ return []
36
+ lines = result.stdout.strip().split('\n')
37
+ items = []
38
+ for line in lines:
39
+ line = line.strip()
40
+ if not line:
41
+ continue
42
+ try:
43
+ items.append(json.loads(line))
44
+ except json.JSONDecodeError:
45
+ continue
46
+ return items
47
+ except subprocess.TimeoutExpired:
48
+ print("yt-dlp timeout")
49
+ return []
50
+ except FileNotFoundError:
51
+ print("yt-dlp not found!")
52
+ return []
53
+ except Exception as e:
54
+ print(f"yt-dlp exception: {e}")
55
+ return []
56
+
57
+ def get_channel_shorts_via_playlist(channel_username, max_count=100):
58
+ """Get shorts from channel's shorts page using yt-dlp"""
59
+ shorts = []
60
+
61
+ # Method 1: Fetch from /shorts page
62
+ url = f"https://www.youtube.com/@{channel_username}/shorts"
63
+ items = run_yt_dlp([
64
+ "--dump-json",
65
+ "--flat-playlist",
66
+ "--no-download",
67
+ "--playlist-end", str(max_count),
68
+ "--no-check-certificates",
69
+ "--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
70
+ url
71
+ ], timeout=90)
72
+
73
+ seen_ids = set()
74
+ for item in items:
75
+ vid = item.get('id', '')
76
+ if not vid or vid in seen_ids:
77
+ continue
78
+ seen_ids.add(vid)
79
+
80
+ title = item.get('title', 'VTV Nam Bộ Short')
81
+ duration = item.get('duration', 0) or 0
82
+
83
+ # Only include actual shorts (<= 120s to be safe)
84
+ if duration <= 120 or '#shorts' in title.lower() or '#short' in title.lower():
85
+ shorts.append({
86
+ 'id': vid,
87
+ 'title': title,
88
+ 'duration': duration,
89
+ 'channel': channel_username,
90
+ 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
91
+ })
92
+
93
+ return shorts
94
+
95
+ def get_channel_videos_filter_shorts(channel_username, max_count=200):
96
+ """Get all videos from /videos page and filter for shorts by duration"""
97
+ shorts = []
98
+
99
+ url = f"https://www.youtube.com/@{channel_username}/videos"
100
+ items = run_yt_dlp([
101
+ "--dump-json",
102
+ "--flat-playlist",
103
+ "--no-download",
104
+ "--playlist-end", str(max_count),
105
+ "--match-filter", "duration > 0 and duration <= 120",
106
+ "--no-check-certificates",
107
+ "--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
108
+ url
109
+ ], timeout=120)
110
+
111
+ seen_ids = set()
112
+ for item in items:
113
+ vid = item.get('id', '')
114
+ if not vid or vid in seen_ids:
115
+ continue
116
+ seen_ids.add(vid)
117
+
118
+ title = item.get('title', 'VTV Nam Bộ Short')
119
+ duration = item.get('duration', 0) or 0
120
+
121
+ shorts.append({
122
+ 'id': vid,
123
+ 'title': title,
124
+ 'duration': duration,
125
+ 'channel': channel_username,
126
+ 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg",
127
+ })
128
+
129
+ return shorts
130
+
131
+ def get_shorts_with_direct_url(video_ids):
132
+ """Get direct video download URLs for given video IDs"""
133
+ results = []
134
+
135
+ for vid in video_ids[:20]: # Limit to avoid timeout
136
+ try:
137
+ url = f"https://www.youtube.com/shorts/{vid}"
138
+ items = run_yt_dlp([
139
+ "--dump-json",
140
+ "--no-download",
141
+ "--no-check-certificates",
142
+ "--format", "best[filesize<10M]/best",
143
+ url
144
+ ], timeout=30)
145
+
146
+ if items:
147
+ info = items[0]
148
+ direct_url = info.get('url', '')
149
+ if not direct_url:
150
+ # Try to get from formats
151
+ formats = info.get('formats', [])
152
+ for f in formats:
153
+ if f.get('vcodec') != 'none' and f.get('acodec') != 'none':
154
+ direct_url = f.get('url', '')
155
+ break
156
+
157
+ if direct_url:
158
+ results.append({
159
+ 'id': vid,
160
+ 'title': info.get('title', ''),
161
+ 'direct_url': direct_url,
162
+ 'thumbnail': info.get('thumbnail', f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"),
163
+ 'duration': info.get('duration', 0),
164
+ })
165
+ except Exception as e:
166
+ print(f"Error getting URL for {vid}: {e}")
167
+
168
+ return results
169
+
170
+ def get_vtvnambo_shorts(max_count=50):
171
+ """Get all shorts from VTV Nam Bộ using yt-dlp"""
172
+ cached = _cached('vtvnambo_shorts_yt')
173
+ if cached is not None:
174
+ return cached
175
+
176
+ all_shorts = []
177
+ seen_ids = set()
178
+
179
+ # Method 1: /shorts page
180
+ print(f"[yt-dlp] Fetching /shorts page...")
181
+ shorts_page = get_channel_shorts_via_playlist('vtvnambo', max_count)
182
+ for s in shorts_page:
183
+ if s['id'] not in seen_ids:
184
+ seen_ids.add(s['id'])
185
+ all_shorts.append(s)
186
+ print(f"[yt-dlp] /shorts page: {len(shorts_page)} shorts")
187
+
188
+ # Method 2: /videos page with duration filter
189
+ if len(all_shorts) < 5:
190
+ print(f"[yt-dlp] Fetching /videos page with filter...")
191
+ videos_filtered = get_channel_videos_filter_shorts('vtvnambo', max_count * 2)
192
+ for s in videos_filtered:
193
+ if s['id'] not in seen_ids:
194
+ seen_ids.add(s['id'])
195
+ all_shorts.append(s)
196
+ print(f"[yt-dlp] /videos filter: {len(videos_filtered)} shorts")
197
+
198
+ result = all_shorts[:max_count]
199
+ _set_cache('vtvnambo_shorts_yt', result)
200
+ print(f"[yt-dlp] Total: {len(result)} shorts from VTV Nam Bộ")
201
+ return result
202
+
203
+ def get_wc_related_shorts(max_count=30):
204
+ """Get World Cup / football related shorts"""
205
+ all_shorts = get_vtvnambo_shorts(max_count * 3)
206
+
207
+ wc_kws = [
208
+ 'world cup', 'wc 2026', 'worldcup', 'fifa', 'bóng đá',
209
+ 'trận đấu', 'đội tuyển', 'tuyển', 'vòng loại',
210
+ 'khoảnh khắc', 'highlights', 'bàn thắng', 'goal',
211
+ 'kết quả', 'tỉ số', 'việt nam', 'vn',
212
+ 'ngoại hạng', 'premier league', 'champions league',
213
+ 'laliga', 'serie a', 'bundesliga', 'ligue 1',
214
+ 'copa', 'europa', 'c1', 'c2',
215
+ 'messi', 'ronaldo', 'neymar', 'mbappe', 'haaland',
216
+ 'v-league', 'vleague', 'bóng đá việt',
217
+ 'đội bóng', 'hlv', 'huấn luyện viên',
218
+ 'chuyển nhượng', 'transfer',
219
+ 'asian cup', 'aff cup', 'sea games',
220
+ 'olympic', 'u23', 'u20', 'u17',
221
+ ]
222
+
223
+ wc_shorts = []
224
+ for s in all_shorts:
225
+ tl = s.get('title', '').lower()
226
+ if any(k in tl for k in wc_kws):
227
+ wc_shorts.append(s)
228
+
229
+ if not wc_shorts:
230
+ wc_shorts = all_shorts
231
+
232
+ return wc_shorts[:max_count]
233
+
234
+ # Aliases
235
+ get_vtvnamo_shorts = get_vtvnambo_shorts