bep40 commited on
Commit
c7da4c9
·
verified ·
1 Parent(s): c2a95f6

Delete static/collect_shorts.html, static/shorts_collector.html, static/test_yt.html

Browse files
static/collect_shorts.html DELETED
@@ -1,196 +0,0 @@
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>Collect VTV Nam Bộ Shorts</title>
7
- <style>
8
- body{background:#111;color:#eee;font-family:monospace;padding:20px;max-width:900px;margin:0 auto}
9
- h1{color:#f0c040;font-size:18px;margin-bottom:10px}
10
- input,button,textarea{font-family:monospace;font-size:13px}
11
- input{width:100%;padding:8px;background:#222;border:1px solid #444;color:#eee;border-radius:6px;margin:6px 0}
12
- button{background:#0b6bcb;border:none;color:#fff;padding:10px 20px;border-radius:6px;cursor:pointer;margin:4px}
13
- button:hover{background:#1a7be0}
14
- textarea{width:100%;height:300px;background:#222;border:1px solid #444;color:#5cb87a;border-radius:6px;padding:8px;margin:6px 0;resize:vertical}
15
- .status{padding:10px;background:#1a1a1a;border-radius:6px;margin:10px 0;white-space:pre-wrap;font-size:12px}
16
- .short-item{padding:6px 10px;background:#1a1a1a;border-radius:4px;margin:4px 0;display:flex;gap:8px;align-items:center}
17
- .short-item img{width:60px;height:106px;object-fit:cover;border-radius:4px;background:#333}
18
- .short-item .info{flex:1}
19
- .short-item .title{font-size:12px;color:#ccc}
20
- .short-item .id{font-size:10px;color:#777}
21
- </style>
22
- </head>
23
- <body>
24
- <h1>📱 Collect YouTube Shorts from @vtvnambo</h1>
25
- <p style="color:#888;font-size:12px">Tool này sẽ fetch trang YouTube của VTV Nam Bộ và extract tất cả shorts. Chạy trên browser của bạn (không qua server).</p>
26
-
27
- <input type="text" id="proxyUrl" placeholder="CORS proxy (leave empty to try direct)" value="">
28
- <button onclick="collectShorts()">🚀 Collect Shorts</button>
29
- <button onclick="collectFromVideos()">📹 Collect from /videos (duration filter)</button>
30
- <button onclick="exportJSON()">📋 Export JSON</button>
31
- <button onclick="exportJS()">📄 Export JS Array</button>
32
-
33
- <div class="status" id="status">Ready...</div>
34
- <div id="results"></div>
35
-
36
- <script>
37
- let collectedShorts = [];
38
-
39
- async function fetchPage(url, useProxy = false) {
40
- const el = document.getElementById('status');
41
- el.textContent = `Fetching: ${url}...`;
42
-
43
- let fetchUrl = url;
44
- const proxy = document.getElementById('proxyUrl').value.trim();
45
- if (proxy) {
46
- fetchUrl = proxy + encodeURIComponent(url);
47
- }
48
-
49
- try {
50
- const r = await fetch(fetchUrl, {
51
- headers: {
52
- 'Accept': 'text/html,*/*'
53
- },
54
- signal: AbortSignal.timeout(15000)
55
- });
56
- if (!r.ok) throw new Error(`HTTP ${r.status}`);
57
- return await r.text();
58
- } catch(e) {
59
- el.textContent += `\nError: ${e.message}`;
60
- throw e;
61
- }
62
- }
63
-
64
- function extractShortsFromHTML(html) {
65
- const shorts = [];
66
- const seen = new Set();
67
-
68
- // Pattern 1: videoId in JSON data
69
- const patterns = [
70
- /"videoId":"([A-Za-z0-9_-]{11})"/g,
71
- /"videoId"\s*:\s*"([A-Za-z0-9_-]{11})"/g,
72
- /watch\?v=([A-Za-z0-9_-]{11})/g,
73
- /\/shorts\/([A-Za-z0-9_-]{11})/g
74
- ];
75
-
76
- for (const pattern of patterns) {
77
- let m;
78
- while ((m = pattern.exec(html)) !== null) {
79
- const vid = m[1];
80
- if (seen.has(vid)) continue;
81
- seen.add(vid);
82
-
83
- // Try to find title near this video ID
84
- const start = Math.max(0, m.index - 1500);
85
- const end = Math.min(html.length, m.index + 1500);
86
- const snippet = html.substring(start, end);
87
-
88
- let title = "VTV Nam Bộ Short";
89
- const titleMatch = snippet.match(/"title"\s*:\s*\{"runs"\s*:\s*\[\{"text"\s*:\s*"([^"]+)"/);
90
- if (titleMatch) {
91
- title = titleMatch[1].replace(/\\n/g, ' ').replace(/\\"/g, '"').trim();
92
- } else {
93
- const titleMatch2 = snippet.match(/"title"\s*:\s*"([^"]+)"/);
94
- if (titleMatch2) {
95
- title = titleMatch2[1].replace(/\\n/g, ' ').trim();
96
- } else {
97
- const titleMatch3 = snippet.match(/"accessibilityText"\s*:\s*"([^"]+)"/);
98
- if (titleMatch3) {
99
- title = titleMatch3[1].replace(/\\n/g, ' ').trim();
100
- }
101
- }
102
- }
103
-
104
- // Check if page is shorts page
105
- const isShortsPage = html.includes('/shorts/') || html.includes('"shorts"');
106
- const titleLower = title.toLowerCase();
107
- const isShort = isShortsPage || titleLower.includes('#shorts') || titleLower.includes('#short');
108
-
109
- shorts.push({
110
- id: vid,
111
- title: title,
112
- channel: 'vtvnambo',
113
- img: `https://i.ytimg.com/vi/${vid}/hqdefault.jpg`,
114
- isShort: isShort
115
- });
116
- }
117
- }
118
-
119
- return shorts;
120
- }
121
-
122
- async function collectShorts() {
123
- const el = document.getElementById('status');
124
- collectedShorts = [];
125
-
126
- try {
127
- // Try shorts page
128
- const html = await fetchPage('https://www.youtube.com/@vtvnambo/shorts');
129
- const shorts = extractShortsFromHTML(html);
130
- collectedShorts = shorts;
131
-
132
- el.textContent = `Found ${shorts.length} videos from /shorts page`;
133
- displayResults(shorts);
134
- } catch(e) {
135
- el.textContent += `\nFailed. Try using a CORS proxy like https://corsproxy.io/?`;
136
- }
137
- }
138
-
139
- async function collectFromVideos() {
140
- const el = document.getElementById('status');
141
- collectedShorts = [];
142
-
143
- try {
144
- const html = await fetchPage('https://www.youtube.com/@vtvnambo/videos');
145
- const all = extractShortsFromHTML(html);
146
-
147
- // First page might have limited results, try to find continuation
148
- collectedShorts = all;
149
-
150
- el.textContent = `Found ${all.length} videos from /videos page`;
151
- displayResults(all);
152
- } catch(e) {
153
- el.textContent += `\nFailed. Try using a CORS proxy.`;
154
- }
155
- }
156
-
157
- function displayResults(shorts) {
158
- const el = document.getElementById('results');
159
- let h = `<h3 style="color:#5cb87a;margin:10px 0">${shorts.length} shorts found:</h3>`;
160
-
161
- shorts.forEach(s => {
162
- h += `<div class="short-item">
163
- <img src="${s.img}" onerror="this.style.background='#333'">
164
- <div class="info">
165
- <div class="title">${s.isShort ? '📱' : '🎬'} ${s.title}</div>
166
- <div class="id">${s.id}</div>
167
- </div>
168
- </div>`;
169
- });
170
-
171
- el.innerHTML = h;
172
- }
173
-
174
- function exportJSON() {
175
- if (!collectedShorts.length) return alert('Chưa collect được shorts nào!');
176
- const json = JSON.stringify(collectedShorts, null, 2);
177
- document.getElementById('status').textContent = json;
178
- navigator.clipboard.writeText(json).then(() => alert('Đã copy JSON!'));
179
- }
180
-
181
- function exportJS() {
182
- if (!collectedShorts.length) return alert('Chưa collect được shorts nào!');
183
- let js = 'const VTV_SHORTS = [\n';
184
- collectedShorts.forEach(s => {
185
- js += ` {"id":"${s.id}","title":${JSON.stringify(s.title)},"channel":"vtvnambo"},\n`;
186
- });
187
- js += '];';
188
- document.getElementById('status').textContent = js;
189
- navigator.clipboard.writeText(js).then(() => alert('Đã copy JS!'));
190
- }
191
-
192
- // Auto-collect on load
193
- setTimeout(collectShorts, 500);
194
- </script>
195
- </body>
196
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/shorts_collector.html DELETED
@@ -1,337 +0,0 @@
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>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
static/test_yt.html DELETED
@@ -1,50 +0,0 @@
1
- <!DOCTYPE html>
2
- <html lang="vi">
3
- <head>
4
- <meta charset="utf-8">
5
- <title>Test yt-dlp</title>
6
- <style>
7
- body{background:#111;color:#eee;font-family:monospace;padding:20px;max-width:800px;margin:0 auto}
8
- button{background:#0b6bcb;border:none;color:#fff;padding:10px 20px;border-radius:6px;cursor:pointer;margin:4px}
9
- pre{background:#1a1a1a;padding:10px;border-radius:6px;overflow:auto;max-height:400px;font-size:11px}
10
- </style>
11
- </head>
12
- <body>
13
- <h2>🧪 Test yt-dlp Scraper</h2>
14
- <button onclick="testAPI()">Test /api/shorts/vtvnamo</button>
15
- <button onclick="testWC()">Test /api/shorts/wc</button>
16
- <pre id="output">Click a button to test...</pre>
17
-
18
- <script>
19
- async function testAPI() {
20
- const el = document.getElementById('output');
21
- el.textContent = 'Loading /api/shorts/vtvnamo...';
22
- try {
23
- const r = await fetch('/api/shorts/vtvnamo?count=50');
24
- const data = await r.json();
25
- el.textContent = `Found ${data.length} shorts:\n\n`;
26
- data.forEach(s => {
27
- el.textContent += `${s.id} | ${s.title}\n`;
28
- });
29
- } catch(e) {
30
- el.textContent = 'Error: ' + e.message;
31
- }
32
- }
33
-
34
- async function testWC() {
35
- const el = document.getElementById('output');
36
- el.textContent = 'Loading /api/shorts/wc...';
37
- try {
38
- const r = await fetch('/api/shorts/wc?count=50');
39
- const data = await r.json();
40
- el.textContent = `Found ${data.length} WC shorts:\n\n`;
41
- data.forEach(s => {
42
- el.textContent += `${s.id} | ${s.title}\n`;
43
- });
44
- } catch(e) {
45
- el.textContent = 'Error: ' + e.message;
46
- }
47
- }
48
- </script>
49
- </body>
50
- </html>