Spaces:
Running
Running
Upload yt_scraper.py
Browse files- yt_scraper.py +16 -106
yt_scraper.py
CHANGED
|
@@ -57,179 +57,89 @@ def run_yt_dlp(args, timeout=120):
|
|
| 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 |
-
|
| 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 |
-
|
| 208 |
-
|
| 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
|
|
|
|
| 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 |
url = f"https://www.youtube.com/@{channel_username}/shorts"
|
| 61 |
items = run_yt_dlp([
|
| 62 |
+
"--dump-json", "--flat-playlist", "--no-download",
|
|
|
|
|
|
|
| 63 |
"--playlist-end", str(max_count),
|
| 64 |
"--no-check-certificates",
|
| 65 |
"--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
| 66 |
url
|
| 67 |
], timeout=90)
|
|
|
|
| 68 |
seen_ids = set()
|
| 69 |
for item in items:
|
| 70 |
vid = item.get('id', '')
|
| 71 |
+
if not vid or vid in seen_ids: continue
|
|
|
|
| 72 |
seen_ids.add(vid)
|
|
|
|
| 73 |
title = item.get('title', 'VTV Nam Bộ Short')
|
| 74 |
duration = item.get('duration', 0) or 0
|
|
|
|
|
|
|
| 75 |
if duration <= 120 or '#shorts' in title.lower() or '#short' in title.lower():
|
| 76 |
+
shorts.append({'id': vid, 'title': title, 'duration': duration, 'channel': channel_username, 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
return shorts
|
| 78 |
|
| 79 |
def get_channel_videos_filter_shorts(channel_username, max_count=200):
|
| 80 |
"""Get all videos from /videos page and filter for shorts by duration"""
|
| 81 |
shorts = []
|
|
|
|
| 82 |
url = f"https://www.youtube.com/@{channel_username}/videos"
|
| 83 |
items = run_yt_dlp([
|
| 84 |
+
"--dump-json", "--flat-playlist", "--no-download",
|
|
|
|
|
|
|
| 85 |
"--playlist-end", str(max_count),
|
| 86 |
"--match-filter", "duration > 0 and duration <= 120",
|
| 87 |
"--no-check-certificates",
|
| 88 |
"--user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
| 89 |
url
|
| 90 |
], timeout=120)
|
|
|
|
| 91 |
seen_ids = set()
|
| 92 |
for item in items:
|
| 93 |
vid = item.get('id', '')
|
| 94 |
+
if not vid or vid in seen_ids: continue
|
|
|
|
| 95 |
seen_ids.add(vid)
|
| 96 |
+
shorts.append({'id': vid, 'title': item.get('title', ''), 'duration': item.get('duration', 0) or 0, 'channel': channel_username, 'img': f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 97 |
return shorts
|
| 98 |
|
| 99 |
def get_shorts_with_direct_url(video_ids):
|
| 100 |
"""Get direct video download URLs for given video IDs"""
|
| 101 |
results = []
|
| 102 |
+
for vid in video_ids[:20]:
|
|
|
|
| 103 |
try:
|
| 104 |
url = f"https://www.youtube.com/shorts/{vid}"
|
| 105 |
+
items = run_yt_dlp(["--dump-json", "--no-download", "--no-check-certificates", "--format", "best[filesize<10M]/best", url], timeout=30)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 106 |
if items:
|
| 107 |
info = items[0]
|
| 108 |
direct_url = info.get('url', '')
|
| 109 |
if not direct_url:
|
|
|
|
| 110 |
formats = info.get('formats', [])
|
| 111 |
for f in formats:
|
| 112 |
if f.get('vcodec') != 'none' and f.get('acodec') != 'none':
|
| 113 |
direct_url = f.get('url', '')
|
| 114 |
break
|
|
|
|
| 115 |
if direct_url:
|
| 116 |
+
results.append({'id': vid, 'title': info.get('title', ''), 'direct_url': direct_url, 'thumbnail': info.get('thumbnail', f"https://i.ytimg.com/vi/{vid}/hqdefault.jpg"), 'duration': info.get('duration', 0)})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 117 |
except Exception as e:
|
| 118 |
print(f"Error getting URL for {vid}: {e}")
|
|
|
|
| 119 |
return results
|
| 120 |
|
| 121 |
def get_vtvnambo_shorts(max_count=50):
|
| 122 |
"""Get all shorts from VTV Nam Bộ using yt-dlp"""
|
| 123 |
cached = _cached('vtvnambo_shorts_yt')
|
| 124 |
+
if cached is not None: return cached
|
| 125 |
+
all_shorts = []; seen_ids = set()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 126 |
shorts_page = get_channel_shorts_via_playlist('vtvnambo', max_count)
|
| 127 |
for s in shorts_page:
|
| 128 |
+
if s['id'] not in seen_ids: seen_ids.add(s['id']); all_shorts.append(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 129 |
if len(all_shorts) < 5:
|
|
|
|
| 130 |
videos_filtered = get_channel_videos_filter_shorts('vtvnambo', max_count * 2)
|
| 131 |
for s in videos_filtered:
|
| 132 |
+
if s['id'] not in seen_ids: seen_ids.add(s['id']); all_shorts.append(s)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
result = all_shorts[:max_count]
|
| 134 |
_set_cache('vtvnambo_shorts_yt', result)
|
|
|
|
| 135 |
return result
|
| 136 |
|
| 137 |
def get_wc_related_shorts(max_count=30):
|
| 138 |
"""Get World Cup / football related shorts"""
|
| 139 |
all_shorts = get_vtvnambo_shorts(max_count * 3)
|
| 140 |
+
wc_kws = ['world cup', 'wc 2026', 'worldcup', 'fifa', 'bóng đá', 'trận đấu', 'đội tuyển', 'tuyển', 'vòng loại', 'khoảnh khắc', 'highlights', 'bàn thắng', 'goal', 'kết quả', 'tỉ số', 'việt nam', 'vn', 'ngoại hạng', 'premier league', 'champions league', 'laliga', 'serie a', 'bundesliga', 'ligue 1', 'copa', 'europa', 'c1', 'c2', 'messi', 'ronaldo', 'neymar', 'mbappe', 'haaland', 'v-league', 'vleague', 'bóng đá việt', 'đội bóng', 'hlv', 'huấn luyện viên', 'chuyển nhượng', 'transfer', 'asian cup', 'aff cup', 'sea games', 'olympic', 'u23', 'u20', 'u17']
|
| 141 |
+
wc_shorts = [s for s in all_shorts if any(k in s.get('title','').lower() for k in wc_kws)]
|
| 142 |
+
if not wc_shorts: wc_shorts = all_shorts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 143 |
return wc_shorts[:max_count]
|
| 144 |
|
|
|
|
| 145 |
get_vtvnamo_shorts = get_vtvnambo_shorts
|