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

Upload static/shorts_collector.html

Browse files
Files changed (1) hide show
  1. static/shorts_collector.html +337 -0
static/shorts_collector.html ADDED
@@ -0,0 +1,337 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="vi">
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1">
6
+ <title>VTV Nam Bộ Shorts Collector</title>
7
+ <style>
8
+ *{box-sizing:border-box;margin:0;padding:0}
9
+ body{background:#0a0a0a;color:#eee;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;padding:16px;max-width:800px;margin:0 auto}
10
+ h1{color:#f0c040;font-size:20px;margin-bottom:8px}
11
+ h2{color:#5cb87a;font-size:14px;margin:16px 0 8px}
12
+ p{color:#888;font-size:12px;margin-bottom:12px}
13
+ button{background:#0b6bcb;border:none;color:#fff;padding:10px 20px;border-radius:8px;font-size:13px;cursor:pointer;margin:4px}
14
+ button:hover{background:#1a7be0}
15
+ button:disabled{background:#333;color:#666;cursor:not-allowed}
16
+ button.danger{background:#c0392b}
17
+ textarea{width:100%;height:200px;background:#1a1a1a;border:1px solid #333;color:#5cb87a;border-radius:8px;padding:10px;font-size:11px;font-family:monospace;resize:vertical}
18
+ .status{background:#1a1a1a;border:1px solid #333;border-radius:8px;padding:12px;margin:10px 0;font-size:12px;white-space:pre-wrap;max-height:300px;overflow-y:auto}
19
+ .short-item{display:flex;gap:10px;padding:8px;background:#1a1a1a;border-radius:6px;margin:4px 0;align-items:center}
20
+ .short-item img{width:80px;height:142px;object-fit:cover;border-radius:4px;background:#333;flex-shrink:0}
21
+ .short-item .info{flex:1;min-width:0}
22
+ .short-item .title{font-size:12px;color:#ccc;word-break:break-word}
23
+ .short-item .id{font-size:10px;color:#666;margin-top:4px}
24
+ .short-item .copy-btn{flex:0 0 auto;background:#333;border:none;color:#ccc;padding:6px 10px;border-radius:4px;font-size:10px;cursor:pointer}
25
+ .short-item .copy-btn:hover{background:#444}
26
+ .grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(180px,1fr));gap:8px;margin:10px 0}
27
+ .grid .card{background:#1a1a1a;border-radius:8px;overflow:hidden;cursor:pointer;border:2px solid transparent;transition:border-color .2s}
28
+ .grid .card:hover{border-color:#0b6bcb}
29
+ .grid .card.selected{border-color:#5cb87a}
30
+ .grid .card img{width:100%;aspect-ratio:9/16;object-fit:cover;background:#333;display:block}
31
+ .grid .card .title{font-size:10px;color:#ccc;padding:4px 6px;line-height:1.3;display:-webkit-box;-webkit-line-clamp:2;-webkit-box-orient:vertical;overflow:hidden}
32
+ .grid .card .id{font-size:9px;color:#666;padding:0 6px 4px}
33
+ .actions{display:flex;gap:8px;flex-wrap:wrap;margin:10px 0}
34
+ </style>
35
+ </head>
36
+ <body>
37
+
38
+ <h1>📱 VTV Nam Bộ Shorts Collector</h1>
39
+ <p>Tool này chạy hoàn toàn trên browser của bạn. Nó sẽ fetch trang YouTube của @vtvnambo và extract tất cả shorts IDs.</p>
40
+
41
+ <h2>Bước 1: Chọn phương pháp</h2>
42
+ <div class="actions">
43
+ <button onclick="methodDirect()">🌐 Method 1: Fetch trực tiếp</button>
44
+ <button onclick="methodProxy()">🔄 Method 2: Qua CORS proxy</button>
45
+ <button onclick="methodManual()">✏️ Method 3: Dán HTML thủ công</button>
46
+ </div>
47
+
48
+ <h2>Bước 2: Kết quả</h2>
49
+ <div class="status" id="status">Nhấn một nút ở trên để bắt đầu...</div>
50
+
51
+ <div class="actions" id="resultActions" style="display:none">
52
+ <button onclick="exportJSON()">📋 Copy JSON</button>
53
+ <button onclick="exportJSArray()">📄 Copy JS Array</button>
54
+ <button onclick="exportPythonList()">🐍 Copy Python List</button>
55
+ <button onclick="testAllVideos()">🧪 Test tất cả videos</button>
56
+ <button class="danger" onclick="clearAll()">🗑 Xóa tất cả</button>
57
+ </div>
58
+
59
+ <div id="results"></div>
60
+
61
+ <h2>Output (copy cái này vào code)</h2>
62
+ <textarea id="output" placeholder="Kết quả sẽ hiện ở đây..."></textarea>
63
+
64
+ <script>
65
+ let allShorts = [];
66
+
67
+ function setStatus(msg) {
68
+ document.getElementById('status').textContent = msg;
69
+ console.log(msg);
70
+ }
71
+
72
+ function addShorts(newShorts) {
73
+ const seen = new Set(allShorts.map(s => s.id));
74
+ let added = 0;
75
+ for (const s of newShorts) {
76
+ if (!seen.has(s.id)) {
77
+ seen.add(s.id);
78
+ allShorts.push(s);
79
+ added++;
80
+ }
81
+ }
82
+ if (added > 0) {
83
+ setStatus(`✅ Thêm ${added} shorts mới. Tổng: ${allShorts.length} shorts.`);
84
+ displayResults();
85
+ updateOutput();
86
+ }
87
+ return added;
88
+ }
89
+
90
+ function extractFromHTML(html, source) {
91
+ const shorts = [];
92
+ const seen = new Set();
93
+
94
+ // Pattern 1: /shorts/VIDEO_ID
95
+ let m;
96
+ const shortsPattern = /\/shorts\/([A-Za-z0-9_-]{11})/g;
97
+ while ((m = shortsPattern.exec(html)) !== null) {
98
+ const vid = m[1];
99
+ if (seen.has(vid)) continue;
100
+ seen.add(vid);
101
+
102
+ // Find title near this match
103
+ const start = Math.max(0, m.index - 2000);
104
+ const end = Math.min(html.length, m.index + 2000);
105
+ const snippet = html.substring(start, end);
106
+
107
+ let title = "VTV Nam Bộ Short";
108
+ const tm = snippet.match(/"title"\s*:\s*\{"runs"\s*:\s*\[\{"text"\s*:\s*"([^"]+)"/);
109
+ if (tm) title = tm[1].replace(/\\n/g,' ').replace(/\\"/g,'"').trim();
110
+
111
+ shorts.push({ id: vid, title, channel: 'vtvnambo', source });
112
+ }
113
+
114
+ // Pattern 2: "videoId":"XXXX" near shorts context
115
+ const vidPattern = /"videoId"\s*:\s*"([A-Za-z0-9_-]{11})"/g;
116
+ while ((m = vidPattern.exec(html)) !== null) {
117
+ const vid = m[1];
118
+ if (seen.has(vid)) continue;
119
+
120
+ // Check if this is in a shorts context
121
+ const start = Math.max(0, m.index - 500);
122
+ const end = Math.min(html.length, m.index + 500);
123
+ const ctx = html.substring(start, end);
124
+
125
+ if (ctx.includes('shorts') || ctx.includes('#shorts') || ctx.includes('#short')) {
126
+ seen.add(vid);
127
+ let title = "VTV Nam Bộ Short";
128
+ const tm = ctx.match(/"title"\s*:\s*\{"runs"\s*:\s*\[\{"text"\s*:\s*"([^"]+)"/);
129
+ if (tm) title = tm[1].replace(/\\n/g,' ').trim();
130
+ shorts.push({ id: vid, title, channel: 'vtvnambo', source });
131
+ }
132
+ }
133
+
134
+ return shorts;
135
+ }
136
+
137
+ // ===== Method 1: Direct fetch =====
138
+ async function methodDirect() {
139
+ setStatus('🔄 Đang fetch trực tiếp từ YouTube...');
140
+
141
+ const urls = [
142
+ 'https://www.youtube.com/@vtvnambo/shorts',
143
+ 'https://www.youtube.com/@vtvnambo/videos',
144
+ ];
145
+
146
+ for (const url of urls) {
147
+ try {
148
+ setStatus(`🔄 Fetching: ${url}...`);
149
+ const r = await fetch(url, {
150
+ headers: { 'Accept': 'text/html' },
151
+ signal: AbortSignal.timeout(15000)
152
+ });
153
+
154
+ if (r.ok) {
155
+ const html = await r.text();
156
+ const shorts = extractFromHTML(html, url);
157
+ setStatus(`✅ Từ ${url}: tìm được ${shorts.length} shorts`);
158
+ addShorts(shorts);
159
+ } else {
160
+ setStatus(`❌ HTTP ${r.status} từ ${url}`);
161
+ }
162
+ } catch(e) {
163
+ setStatus(`❌ Lỗi fetch ${url}: ${e.message}`);
164
+ }
165
+ }
166
+
167
+ if (allShorts.length === 0) {
168
+ setStatus('❌ Fetch trực tiếp thất bại (CORS blocked). Thử Method 2 hoặc 3.');
169
+ }
170
+ }
171
+
172
+ // ===== Method 2: CORS proxy =====
173
+ async function methodProxy() {
174
+ setStatus('🔄 Đang fetch qua CORS proxy...');
175
+
176
+ const proxies = [
177
+ 'https://corsproxy.io/?',
178
+ 'https://api.allorigins.win/raw?url=',
179
+ 'https://cors-anywhere.herokuapp.com/',
180
+ ];
181
+
182
+ const targetUrl = 'https://www.youtube.com/@vtvnambo/shorts';
183
+
184
+ for (const proxy of proxies) {
185
+ try {
186
+ setStatus(`🔄 Thử proxy: ${proxy.substring(0, 40)}...`);
187
+ const fetchUrl = proxy + encodeURIComponent(targetUrl);
188
+ const r = await fetch(fetchUrl, { signal: AbortSignal.timeout(20000) });
189
+
190
+ if (r.ok) {
191
+ const html = await r.text();
192
+ const shorts = extractFromHTML(html, 'proxy');
193
+ setStatus(`✅ Qua proxy: tìm được ${shorts.length} shorts`);
194
+ addShorts(shorts);
195
+ if (shorts.length > 0) return; // Success!
196
+ } else {
197
+ setStatus(`❌ Proxy trả về HTTP ${r.status}`);
198
+ }
199
+ } catch(e) {
200
+ setStatus(`❌ Proxy lỗi: ${e.message.substring(0, 80)}`);
201
+ }
202
+ }
203
+
204
+ // Also try /videos page
205
+ try {
206
+ const proxy = proxies[0];
207
+ const targetUrl2 = 'https://www.youtube.com/@vtvnambo/videos';
208
+ setStatus(`🔄 Thử fetch /videos page...`);
209
+ const r = await fetch(proxy + encodeURIComponent(targetUrl2), { signal: AbortSignal.timeout(20000) });
210
+ if (r.ok) {
211
+ const html = await r.text();
212
+ const shorts = extractFromHTML(html, 'proxy-videos');
213
+ setStatus(`✅ Từ /videos: tìm được ${shorts.length} shorts`);
214
+ addShorts(shorts);
215
+ }
216
+ } catch(e) {
217
+ setStatus(`❌ /videos cũng lỗi: ${e.message.substring(0, 80)}`);
218
+ }
219
+
220
+ if (allShorts.length === 0) {
221
+ setStatus('❌ Tất cả proxy đều thất bại. Dùng Method 3: Dán HTML thủ công.');
222
+ }
223
+ }
224
+
225
+ // ===== Method 3: Manual paste =====
226
+ async function methodManual() {
227
+ setStatus('📋 Hướng dẫn:\n1. Mở https://www.youtube.com/@vtvnambo/shorts trên trình duyệt\n2. Chuột phải → "View Page Source" (hoặc Ctrl+U)\n3. Copy toàn bộ HTML\n4. Dán vào ô bên dưới\n5. Nhấn "Extract"');
228
+
229
+ const html = prompt('Dán HTML source của trang YouTube @vtvnambo/shorts vào đây:');
230
+ if (!html) return;
231
+
232
+ setStatus('🔄 Đang extract từ HTML...');
233
+ const shorts = extractFromHTML(html, 'manual');
234
+ setStatus(`✅ Extract được ${shorts.length} shorts từ HTML`);
235
+ addShorts(shorts);
236
+ }
237
+
238
+ // ===== Display results =====
239
+ function displayResults() {
240
+ const el = document.getElementById('results');
241
+ document.getElementById('resultActions').style.display = 'flex';
242
+
243
+ let h = `<h2>Tìm được ${allShorts.length} shorts:</h2><div class="grid">`;
244
+ allShorts.forEach((s, i) => {
245
+ h += `<div class="card" onclick="toggleSelect(${i})" id="card-${i}">
246
+ <img src="https://i.ytimg.com/vi/${s.id}/hqdefault.jpg" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 width=%2280%22 height=%22142%22><rect fill=%22%23333%22 width=%2280%22 height=%22142%22/><text fill=%22%23666%22 x=%2250%22 y=%2276%22 text-anchor=%22middle%22 font-size=%2210%22>📱</text></svg>'">
247
+ <div class="title">${s.title}</div>
248
+ <div class="id">${s.id}</div>
249
+ </div>`;
250
+ });
251
+ h += '</div>';
252
+ el.innerHTML = h;
253
+ }
254
+
255
+ function toggleSelect(i) {
256
+ const card = document.getElementById('card-' + i);
257
+ card.classList.toggle('selected');
258
+ }
259
+
260
+ // ===== Export =====
261
+ function updateOutput() {
262
+ document.getElementById('output').value = exportJSONString();
263
+ }
264
+
265
+ function exportJSONString() {
266
+ return JSON.stringify(allShorts.map(s => ({
267
+ id: s.id,
268
+ title: s.title,
269
+ channel: 'vtvnambo'
270
+ })), null, 2);
271
+ }
272
+
273
+ function exportJSON() {
274
+ const json = exportJSONString();
275
+ navigator.clipboard.writeText(json).then(() => {
276
+ setStatus('✅ Đã copy JSON vào clipboard!\n\n' + json.substring(0, 200) + '...');
277
+ });
278
+ }
279
+
280
+ function exportJSArray() {
281
+ let js = 'const VTV_SHORTS = [\n';
282
+ allShorts.forEach(s => {
283
+ js += ` {"id":"${s.id}","title":${JSON.stringify(s.title)},"channel":"vtvnambo"},\n`;
284
+ });
285
+ js += '];';
286
+ navigator.clipboard.writeText(js).then(() => setStatus('✅ Đã copy JS Array!'));
287
+ }
288
+
289
+ function exportPythonList() {
290
+ let py = 'VTV_SHORTS = [\n';
291
+ allShorts.forEach(s => {
292
+ py += ` {"id": "${s.id}", "title": """${s.title.replace(/"""/g, '\\"\\"\\"')}""", "channel": "vtvnambo"},\n`;
293
+ });
294
+ py += ']';
295
+ navigator.clipboard.writeText(py).then(() => setStatus('✅ Đã copy Python list!'));
296
+ }
297
+
298
+ // ===== Test videos =====
299
+ async function testAllVideos() {
300
+ setStatus('🧪 Đang test tất cả videos...');
301
+ let working = 0, broken = 0;
302
+
303
+ for (const s of allShorts) {
304
+ try {
305
+ const r = await fetch(`https://i.ytimg.com/vi/${s.id}/hqdefault.jpg`, {
306
+ method: 'HEAD',
307
+ signal: AbortSignal.timeout(5000)
308
+ });
309
+ if (r.ok) {
310
+ working++;
311
+ } else {
312
+ broken++;
313
+ setStatus(`❌ Broken: ${s.id}`);
314
+ }
315
+ } catch(e) {
316
+ broken++;
317
+ }
318
+ }
319
+
320
+ setStatus(`✅ Test xong: ${working} working, ${broken} broken`);
321
+ }
322
+
323
+ function clearAll() {
324
+ allShorts = [];
325
+ document.getElementById('results').innerHTML = '';
326
+ document.getElementById('resultActions').style.display = 'none';
327
+ document.getElementById('output').value = '';
328
+ setStatus('Đã xóa tất cả. Nhấn nút để collect lại.');
329
+ }
330
+
331
+ // Auto-try on load
332
+ setTimeout(() => {
333
+ setStatus('🚀 Tự động thử fetch...\nNhấn "Method 1" hoặc "Method 2" để bắt đầu.');
334
+ }, 500);
335
+ </script>
336
+ </body>
337
+ </html>