alfabill commited on
Commit
f4e73a4
·
verified ·
1 Parent(s): 3ed5c90

Upload folder using huggingface_hub

Browse files
Files changed (1) hide show
  1. index.html +666 -19
index.html CHANGED
@@ -1,19 +1,666 @@
1
- <!doctype html>
2
- <html>
3
- <head>
4
- <meta charset="utf-8" />
5
- <meta name="viewport" content="width=device-width" />
6
- <title>My static Space</title>
7
- <link rel="stylesheet" href="style.css" />
8
- </head>
9
- <body>
10
- <div class="card">
11
- <h1>Welcome to your static Space!</h1>
12
- <p>You can modify this app directly by editing <i>index.html</i> in the Files and versions tab.</p>
13
- <p>
14
- Also don't forget to check the
15
- <a href="https://huggingface.co/docs/hub/spaces" target="_blank">Spaces documentation</a>.
16
- </p>
17
- </div>
18
- </body>
19
- </html>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="es">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>YouTube Shorts Scraper Pro</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet">
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --primary: #ff0000;
13
+ --secondary: #282828;
14
+ --accent: #00d4ff;
15
+ --bg-dark: #0f0f0f;
16
+ --card-bg: rgba(255, 255, 255, 0.05);
17
+ }
18
+
19
+ * {
20
+ margin: 0;
21
+ padding: 0;
22
+ box-sizing: border-box;
23
+ }
24
+
25
+ body {
26
+ font-family: 'Inter', sans-serif;
27
+ background: var(--bg-dark);
28
+ color: white;
29
+ min-height: 100vh;
30
+ overflow-x: hidden;
31
+ }
32
+
33
+ /* Animated Background */
34
+ .bg-gradient {
35
+ position: fixed;
36
+ top: 0;
37
+ left: 0;
38
+ width: 100%;
39
+ height: 100%;
40
+ background:
41
+ radial-gradient(circle at 20% 50%, rgba(255, 0, 0, 0.15) 0%, transparent 50%),
42
+ radial-gradient(circle at 80% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%),
43
+ radial-gradient(circle at 40% 20%, rgba(255, 0, 0, 0.1) 0%, transparent 50%);
44
+ z-index: -1;
45
+ animation: pulse 15s ease-in-out infinite;
46
+ }
47
+
48
+ @keyframes pulse {
49
+ 0%, 100% { opacity: 0.5; transform: scale(1); }
50
+ 50% { opacity: 0.8; transform: scale(1.1); }
51
+ }
52
+
53
+ /* Glassmorphism */
54
+ .glass {
55
+ background: var(--card-bg);
56
+ backdrop-filter: blur(12px);
57
+ -webkit-backdrop-filter: blur(12px);
58
+ border: 1px solid rgba(255, 255, 255, 0.1);
59
+ }
60
+
61
+ .glass-card {
62
+ background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%);
63
+ backdrop-filter: blur(10px);
64
+ border: 1px solid rgba(255,255,255,0.1);
65
+ transition: all 0.3s ease;
66
+ }
67
+
68
+ .glass-card:hover {
69
+ transform: translateY(-5px);
70
+ border-color: rgba(255, 0, 0, 0.5);
71
+ box-shadow: 0 10px 30px rgba(255, 0, 0, 0.2);
72
+ }
73
+
74
+ /* Custom Scrollbar */
75
+ ::-webkit-scrollbar {
76
+ width: 8px;
77
+ }
78
+ ::-webkit-scrollbar-track {
79
+ background: var(--bg-dark);
80
+ }
81
+ ::-webkit-scrollbar-thumb {
82
+ background: #333;
83
+ border-radius: 4px;
84
+ }
85
+ ::-webkit-scrollbar-thumb:hover {
86
+ background: var(--primary);
87
+ }
88
+
89
+ /* Loading Animation */
90
+ .loader {
91
+ width: 48px;
92
+ height: 48px;
93
+ border: 3px solid #FFF;
94
+ border-radius: 50%;
95
+ display: inline-block;
96
+ position: relative;
97
+ box-sizing: border-box;
98
+ animation: rotation 1s linear infinite;
99
+ }
100
+ .loader::after {
101
+ content: '';
102
+ box-sizing: border-box;
103
+ position: absolute;
104
+ left: 50%;
105
+ top: 50%;
106
+ transform: translate(-50%, -50%);
107
+ width: 40px;
108
+ height: 40px;
109
+ border-radius: 50%;
110
+ border: 3px solid transparent;
111
+ border-bottom-color: #ff0000;
112
+
113
+ }
114
+ @keyframes rotation {
115
+ 0% { transform: rotate(0deg); }
116
+ 100% { transform: rotate(360deg); }
117
+ }
118
+
119
+ /* Code Block Styling */
120
+ .code-block {
121
+ font-family: 'JetBrains Mono', monospace;
122
+ background: #1e1e1e;
123
+ border-radius: 8px;
124
+ padding: 1.5rem;
125
+ overflow-x: auto;
126
+ position: relative;
127
+ }
128
+ .code-header {
129
+ display: flex;
130
+ justify-content: space-between;
131
+ align-items: center;
132
+ margin-bottom: 1rem;
133
+ padding-bottom: 1rem;
134
+ border-bottom: 1px solid #333;
135
+ }
136
+ .syntax-keyword { color: #ff79c6; }
137
+ .syntax-string { color: #f1fa8c; }
138
+ .syntax-function { color: #50fa7b; }
139
+ .syntax-comment { color: #6272a4; }
140
+
141
+ /* Input Styling */
142
+ .custom-input {
143
+ background: rgba(255,255,255,0.05);
144
+ border: 2px solid rgba(255,255,255,0.1);
145
+ transition: all 0.3s;
146
+ }
147
+ .custom-input:focus {
148
+ outline: none;
149
+ border-color: var(--primary);
150
+ background: rgba(255,255,255,0.1);
151
+ }
152
+
153
+ /* Grid Animation */
154
+ @keyframes fadeInUp {
155
+ from {
156
+ opacity: 0;
157
+ transform: translateY(20px);
158
+ }
159
+ to {
160
+ opacity: 1;
161
+ transform: translateY(0);
162
+ }
163
+ }
164
+ .animate-in {
165
+ animation: fadeInUp 0.5s ease forwards;
166
+ }
167
+
168
+ /* Responsive Grid */
169
+ .shorts-grid {
170
+ display: grid;
171
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
172
+ gap: 1.5rem;
173
+ }
174
+
175
+ @media (max-width: 640px) {
176
+ .shorts-grid {
177
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
178
+ gap: 1rem;
179
+ }
180
+ }
181
+
182
+ /* AnyCoder Link */
183
+ .anycoder-link {
184
+ background: linear-gradient(90deg, #ff0000, #ff6b6b);
185
+ -webkit-background-clip: text;
186
+ -webkit-text-fill-color: transparent;
187
+ font-weight: 700;
188
+ text-decoration: none;
189
+ position: relative;
190
+ }
191
+ .anycoder-link::after {
192
+ content: '';
193
+ position: absolute;
194
+ bottom: -2px;
195
+ left: 0;
196
+ width: 0;
197
+ height: 2px;
198
+ background: linear-gradient(90deg, #ff0000, #ff6b6b);
199
+ transition: width 0.3s;
200
+ }
201
+ .anycoder-link:hover::after {
202
+ width: 100%;
203
+ }
204
+
205
+ /* Toast Notification */
206
+ .toast {
207
+ position: fixed;
208
+ bottom: 20px;
209
+ right: 20px;
210
+ background: rgba(0,0,0,0.9);
211
+ border-left: 4px solid var(--primary);
212
+ padding: 1rem 2rem;
213
+ border-radius: 4px;
214
+ transform: translateX(400px);
215
+ transition: transform 0.3s ease;
216
+ z-index: 1000;
217
+ }
218
+ .toast.show {
219
+ transform: translateX(0);
220
+ }
221
+ </style>
222
+ </head>
223
+ <body>
224
+ <div class="bg-gradient"></div>
225
+
226
+ <!-- Header -->
227
+ <header class="glass sticky top-0 z-50 border-b border-white/10">
228
+ <div class="container mx-auto px-4 py-4 flex justify-between items-center">
229
+ <div class="flex items-center gap-3">
230
+ <div class="w-10 h-10 bg-red-600 rounded-lg flex items-center justify-center">
231
+ <i class="fab fa-youtube text-white text-xl"></i>
232
+ </div>
233
+ <div>
234
+ <h1 class="text-xl font-bold tracking-tight">Shorts Scraper <span class="text-red-500">Pro</span></h1>
235
+ <p class="text-xs text-gray-400">Node.js Powered</p>
236
+ </div>
237
+ </div>
238
+ <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link text-sm">
239
+ Built with anycoder <i class="fas fa-external-link-alt ml-1 text-xs"></i>
240
+ </a>
241
+ </div>
242
+ </header>
243
+
244
+ <main class="container mx-auto px-4 py-8 max-w-7xl">
245
+ <!-- Hero Section -->
246
+ <section class="text-center mb-12">
247
+ <h2 class="text-4xl md:text-6xl font-bold mb-4 bg-gradient-to-r from-white to-gray-400 bg-clip-text text-transparent">
248
+ Extrae Shorts de YouTube
249
+ </h2>
250
+ <p class="text-gray-400 text-lg max-w-2xl mx-auto mb-8">
251
+ Simulación de interfaz para scraping de contenido. En un entorno real, esto requeriría un backend Node.js con Puppeteer o yt-dlp.
252
+ </p>
253
+ </section>
254
+
255
+ <!-- Control Panel -->
256
+ <section class="glass rounded-2xl p-6 mb-12 max-w-4xl mx-auto">
257
+ <div class="grid md:grid-cols-3 gap-4 mb-6">
258
+ <div class="md:col-span-2">
259
+ <label class="block text-sm font-medium mb-2 text-gray-300">URL del Canal o Búsqueda</label>
260
+ <div class="relative">
261
+ <i class="fas fa-link absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i>
262
+ <input type="text" id="channelInput" placeholder="https://youtube.com/@channel o término de búsqueda"
263
+ class="custom-input w-full pl-10 pr-4 py-3 rounded-lg text-white placeholder-gray-500">
264
+ </div>
265
+ </div>
266
+ <div>
267
+ <label class="block text-sm font-medium mb-2 text-gray-300">Límite de Shorts</label>
268
+ <select id="limitSelect" class="custom-input w-full px-4 py-3 rounded-lg text-white cursor-pointer">
269
+ <option value="10">10 shorts</option>
270
+ <option value="25" selected>25 shorts</option>
271
+ <option value="50">50 shorts</option>
272
+ <option value="100">100 shorts</option>
273
+ </select>
274
+ </div>
275
+ </div>
276
+
277
+ <div class="flex flex-wrap gap-3 mb-6">
278
+ <button onclick="toggleAdvanced()" class="text-sm text-gray-400 hover:text-white transition flex items-center gap-2">
279
+ <i class="fas fa-cog"></i> Opciones Avanzadas
280
+ </button>
281
+ </div>
282
+
283
+ <div id="advancedOptions" class="hidden mb-6 p-4 bg-black/20 rounded-lg border border-white/5">
284
+ <div class="grid md:grid-cols-3 gap-4">
285
+ <label class="flex items-center gap-2 cursor-pointer">
286
+ <input type="checkbox" id="includeMetadata" checked class="w-4 h-4 rounded border-gray-600 text-red-600 focus:ring-red-600 bg-gray-700">
287
+ <span class="text-sm text-gray-300">Incluir Metadata</span>
288
+ </label>
289
+ <label class="flex items-center gap-2 cursor-pointer">
290
+ <input type="checkbox" id="hdQuality" checked class="w-4 h-4 rounded border-gray-600 text-red-600 focus:ring-red-600 bg-gray-700">
291
+ <span class="text-sm text-gray-300">Alta Calidad</span>
292
+ </label>
293
+ <label class="flex items-center gap-2 cursor-pointer">
294
+ <input type="checkbox" id="autoDownload" class="w-4 h-4 rounded border-gray-600 text-red-600 focus:ring-red-600 bg-gray-700">
295
+ <span class="text-sm text-gray-300">Auto-descargar</span>
296
+ </label>
297
+ </div>
298
+ </div>
299
+
300
+ <button onclick="startScraping()" id="scrapeBtn"
301
+ class="w-full bg-gradient-to-r from-red-600 to-red-700 hover:from-red-700 hover:to-red-800 text-white font-bold py-4 rounded-lg transition-all transform hover:scale-[1.02] flex items-center justify-center gap-2 shadow-lg shadow-red-600/20">
302
+ <i class="fas fa-play"></i>
303
+ Iniciar Scraping
304
+ </button>
305
+
306
+ <!-- Progress Bar -->
307
+ <div id="progressContainer" class="hidden mt-6">
308
+ <div class="flex justify-between text-sm mb-2 text-gray-400">
309
+ <span id="progressText">Iniciando navegador...</span>
310
+ <span id="progressPercent">0%</span>
311
+ </div>
312
+ <div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden">
313
+ <div id="progressBar" class="bg-gradient-to-r from-red-500 to-red-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
314
+ </div>
315
+ </div>
316
+ </section>
317
+
318
+ <!-- Results Section -->
319
+ <section id="resultsSection" class="hidden">
320
+ <div class="flex justify-between items-center mb-6">
321
+ <h3 class="text-2xl font-bold flex items-center gap-2">
322
+ <i class="fas fa-film text-red-500"></i>
323
+ Resultados
324
+ <span id="resultCount" class="text-sm bg-red-600 px-2 py-1 rounded-full">0</span>
325
+ </h3>
326
+ <div class="flex gap-2">
327
+ <button onclick="exportJSON()" class="glass px-4 py-2 rounded-lg text-sm hover:bg-white/10 transition flex items-center gap-2">
328
+ <i class="fas fa-download"></i> Exportar JSON
329
+ </button>
330
+ <button onclick="clearResults()" class="glass px-4 py-2 rounded-lg text-sm hover:bg-white/10 transition text-red-400">
331
+ <i class="fas fa-trash"></i> Limpiar
332
+ </button>
333
+ </div>
334
+ </div>
335
+
336
+ <div id="shortsContainer" class="shorts-grid">
337
+ <!-- Shorts will be injected here -->
338
+ </div>
339
+ </section>
340
+
341
+ <!-- Node.js Code Reference -->
342
+ <section class="mt-16 mb-12">
343
+ <div class="glass rounded-2xl p-8">
344
+ <div class="flex items-center justify-between mb-6">
345
+ <div>
346
+ <h3 class="text-2xl font-bold mb-2">Implementación Node.js Real</h3>
347
+ <p class="text-gray-400 text-sm">Código necesario para ejecutar esto en un servidor real</p>
348
+ </div>
349
+ <button onclick="copyCode()" class="bg-white/10 hover:bg-white/20 px-4 py-2 rounded-lg transition text-sm flex items-center gap-2">
350
+ <i class="fas fa-copy"></i> Copiar
351
+ </button>
352
+ </div>
353
+
354
+ <div class="code-block text-sm">
355
+ <div class="code-header">
356
+ <span class="text-gray-400">scraper.js</span>
357
+ <div class="flex gap-2">
358
+ <div class="w-3 h-3 rounded-full bg-red-500"></div>
359
+ <div class="w-3 h-3 rounded-full bg-yellow-500"></div>
360
+ <div class="w-3 h-3 rounded-full bg-green-500"></div>
361
+ </div>
362
+ </div>
363
+ <pre><code><span class="syntax-keyword">const</span> puppeteer = <span class="syntax-function">require</span>(<span class="syntax-string">'puppeteer'</span>);
364
+ <span class="syntax-keyword">const</span> fs = <span class="syntax-function">require</span>(<span class="syntax-string">'fs'</span>);
365
+
366
+ <span class="syntax-keyword">async function</span> <span class="syntax-function">scrapeShorts</span>(channelUrl, limit = <span class="syntax-string">25</span>) {
367
+ <span class="syntax-keyword">const</span> browser = <span class="syntax-keyword">await</span> puppeteer.<span class="syntax-function">launch</span>({ headless: <span class="syntax-string">true</span> });
368
+ <span class="syntax-keyword">const</span> page = <span class="syntax-keyword">await</span> browser.<span class="syntax-function">newPage</span>();
369
+
370
+ <span class="syntax-comment">// Navegar al canal</span>
371
+ <span class="syntax-keyword">await</span> page.<span class="syntax-function">goto</span>(<span class="syntax-string">`</span>${channelUrl}<span class="syntax-string">/shorts`</span>, {
372
+ waitUntil: <span class="syntax-string">'networkidle2'</span>
373
+ });
374
+
375
+ <span class="syntax-comment">// Scroll infinito para cargar shorts</span>
376
+ <span class="syntax-keyword">let</span> shorts = [];
377
+ <span class="syntax-keyword">let</span> previousHeight = <span class="syntax-string">0</span>;
378
+
379
+ <span class="syntax-keyword">while</span> (shorts.length < limit) {
380
+ <span class="syntax-keyword">const</span> newShorts = <span class="syntax-keyword">await</span> page.<span class="syntax-function">evaluate</span>(() => {
381
+ <span class="syntax-keyword">const</span> items = document.<span class="syntax-function">querySelectorAll</span>(<span class="syntax-string">'ytd-reel-item-renderer'</span>);
382
+ <span class="syntax-keyword">return</span> Array.<span class="syntax-function">from</span>(items).<span class="syntax-function">map</span>(item => ({
383
+ title: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'#video-title'</span>)?.innerText,
384
+ views: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string'>'.style-scope ytd-grid-video-renderer'</span>)?.innerText,
385
+ url: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'a'</span>)?.href,
386
+ thumbnail: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'img'</span>)?.src
387
+ }));
388
+ });
389
+
390
+ shorts = [...shorts, ...newShorts];
391
+
392
+ <span class="syntax-comment">// Scroll down</span>
393
+ <span class="syntax-keyword">await</span> page.<span class="syntax-function">evaluate</span>(<span class="syntax-string">'window.scrollTo(0, document.body.scrollHeight)'</span>);
394
+ <span class="syntax-keyword">await</span> page.<span class="syntax-function">waitForTimeout</span>(<span class="syntax-string">2000</span>);
395
+ }
396
+
397
+ <span class="syntax-keyword">await</span> browser.<span class="syntax-function">close</span>();
398
+ <span class="syntax-keyword">return</span> shorts.<span class="syntax-function">slice</span>(<span class="syntax-string">0</span>, limit);
399
+ }
400
+
401
+ <span class="syntax-comment">// Ejecutar</span>
402
+ <span class="syntax-function">scrapeShorts</span>(process.argv[<span class="syntax-string">2</span>], <span class="syntax-string">25</span>)
403
+ .<span class="syntax-function">then</span>(data => {
404
+ fs.<span class="syntax-function">writeFileSync</span>(<span class="syntax-string">'shorts.json'</span>, JSON.<span class="syntax-function">stringify</span>(data, <span class="syntax-string">null</span>, <span class="syntax-string">2</span>));
405
+ console.<span class="syntax-function">log</span>(<span class="syntax-string">'✅ Scraping completado'</span>);
406
+ });</code></pre>
407
+ </div>
408
+
409
+ <div class="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg">
410
+ <p class="text-sm text-yellow-200 flex items-start gap-2">
411
+ <i class="fas fa-exclamation-triangle mt-1"></i>
412
+ <span>
413
+ <strong>Nota importante:</strong> YouTube tiene protecciones contra scraping (CORS, rate limiting, CAPTCHAs).
414
+ Para uso real, se recomienda usar la API oficial de YouTube Data API v3 o yt-dlp con proxies rotativos.
415
+ </span>
416
+ </p>
417
+ </div>
418
+ </div>
419
+ </section>
420
+ </main>
421
+
422
+ <!-- Toast Notification -->
423
+ <div id="toast" class="toast">
424
+ <div class="flex items-center gap-3">
425
+ <i class="fas fa-check-circle text-green-500"></i>
426
+ <span id="toastMessage">Operación completada</span>
427
+ </div>
428
+ </div>
429
+
430
+ <script>
431
+ // Mock Data Generator
432
+ const mockTitles = [
433
+ "¡Increíble truco de magia! 🎩✨",
434
+ "Resumen del partido ⚽🔥",
435
+ "Tutorial rápido de cocina 🍳",
436
+ "Reacción épica 😱",
437
+ "Datos curiosos que no sabías 🤯",
438
+ "Behind the scenes 🎬",
439
+ "Vlog diario 📹",
440
+ "Challenge accepted 💪",
441
+ "Un día en mi vida 🌅",
442
+ "Review honesto ⭐"
443
+ ];
444
+
445
+ const mockChannels = [
446
+ "CreatorPro", "ShortsMaster", "ViralClips", "DailyDose",
447
+ "TechTips", "FoodieLife", "GamingZone", "FitnessDaily"
448
+ ];
449
+
450
+ function generateMockShorts(count) {
451
+ const shorts = [];
452
+ for (let i = 0; i < count; i++) {
453
+ const id = Math.random().toString(36).substr(2, 9);
454
+ const views = Math.floor(Math.random() * 999) + 1;
455
+ const likes = Math.floor(Math.random() * 50) + 1;
456
+
457
+ shorts.push({
458
+ id: id,
459
+ title: mockTitles[Math.floor(Math.random() * mockTitles.length)],
460
+ channel: mockChannels[Math.floor(Math.random() * mockChannels.length)],
461
+ views: views > 900 ? `${views}K` : `${views}K`,
462
+ likes: likes,
463
+ duration: `${Math.floor(Math.random() * 50) + 10}s`,
464
+ thumbnail: `https://picsum.photos/seed/${id}/400/700`,
465
+ url: `https://youtube.com/shorts/${id}`
466
+ });
467
+ }
468
+ return shorts;
469
+ }
470
+
471
+ // UI Functions
472
+ function toggleAdvanced() {
473
+ const options = document.getElementById('advancedOptions');
474
+ options.classList.toggle('hidden');
475
+ }
476
+
477
+ function showToast(message) {
478
+ const toast = document.getElementById('toast');
479
+ document.getElementById('toastMessage').textContent = message;
480
+ toast.classList.add('show');
481
+ setTimeout(() => toast.classList.remove('show'), 3000);
482
+ }
483
+
484
+ async function startScraping() {
485
+ const input = document.getElementById('channelInput').value;
486
+ if (!input) {
487
+ showToast('Por favor ingresa una URL o término de búsqueda');
488
+ return;
489
+ }
490
+
491
+ const btn = document.getElementById('scrapeBtn');
492
+ const progressContainer = document.getElementById('progressContainer');
493
+ const progressBar = document.getElementById('progressBar');
494
+ const progressText = document.getElementById('progressText');
495
+ const progressPercent = document.getElementById('progressPercent');
496
+ const limit = parseInt(document.getElementById('limitSelect').value);
497
+
498
+ // Reset UI
499
+ btn.disabled = true;
500
+ btn.innerHTML = '<span class="loader scale-50 mr-2"></span> Procesando...';
501
+ progressContainer.classList.remove('hidden');
502
+ document.getElementById('resultsSection').classList.add('hidden');
503
+
504
+ // Simulate progress
505
+ const steps = [
506
+ { percent: 10, text: 'Iniciando navegador headless...' },
507
+ { percent: 25, text: 'Navegando al canal...' },
508
+ { percent: 40, text: 'Extrayendo URLs de shorts...' },
509
+ { percent: 60, text: 'Obteniendo metadata...' },
510
+ { percent: 80, text: 'Procesando thumbnails...' },
511
+ { percent: 100, text: 'Completado!' }
512
+ ];
513
+
514
+ for (let step of steps) {
515
+ await new Promise(r => setTimeout(r, 800));
516
+ progressBar.style.width = step.percent + '%';
517
+ progressText.textContent = step.text;
518
+ progressPercent.textContent = step.percent + '%';
519
+ }
520
+
521
+ // Generate results
522
+ const shorts = generateMockShorts(limit);
523
+ displayResults(shorts);
524
+
525
+ // Reset button
526
+ btn.disabled = false;
527
+ btn.innerHTML = '<i class="fas fa-play"></i> Iniciar Scraping';
528
+ progressContainer.classList.add('hidden');
529
+ showToast(`Se encontraron ${shorts.length} shorts`);
530
+ }
531
+
532
+ function displayResults(shorts) {
533
+ const container = document.getElementById('shortsContainer');
534
+ const resultsSection = document.getElementById('resultsSection');
535
+ const countBadge = document.getElementById('resultCount');
536
+
537
+ container.innerHTML = '';
538
+ countBadge.textContent = shorts.length;
539
+ resultsSection.classList.remove('hidden');
540
+
541
+ shorts.forEach((short, index) => {
542
+ const card = document.createElement('div');
543
+ card.className = 'glass-card rounded-xl overflow-hidden animate-in';
544
+ card.style.animationDelay = `${index * 0.05}s`;
545
+
546
+ card.innerHTML = `
547
+ <div class="relative aspect-[9/16] bg-gray-800 overflow-hidden group">
548
+ <img src="${short.thumbnail}" alt="${short.title}"
549
+ class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110"
550
+ onerror="this.src='https://via.placeholder.com/400x700/222/fff?text=Short'">
551
+ <div class="absolute inset-0 bg-gradient-to-t from-black/80 via-transparent to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300"></div>
552
+ <span class="absolute bottom-2 right-2 bg-black/80 px-2 py-1 rounded text-xs font-mono">${short.duration}</span>
553
+ <button onclick="copyUrl('${short.url}')" class="absolute top-2 right-2 bg-black/50 hover:bg-red-600 p-2 rounded-full opacity-0 group-hover:opacity-100 transition-all duration-300">
554
+ <i class="fas fa-link text-xs"></i>
555
+ </button>
556
+ </div>
557
+ <div class="p-4">
558
+ <h4 class="font-semibold text-sm mb-2 line-clamp-2 leading-tight">${short.title}</h4>
559
+ <div class="flex items-center justify-between text-xs text-gray-400 mb-3">
560
+ <span class="flex items-center gap-1">
561
+ <i class="fas fa-user-circle"></i> ${short.channel}
562
+ </span>
563
+ <span class="flex items-center gap-1">
564
+ <i class="fas fa-eye"></i> ${short.views}
565
+ </span>
566
+ </div>
567
+ <div class="flex gap-2">
568
+ <button onclick="downloadShort('${short.id}')" class="flex-1 bg-red-600 hover:bg-red-700 text-white text-xs py-2 rounded transition flex items-center justify-center gap-1">
569
+ <i class="fas fa-download"></i> Descargar
570
+ </button>
571
+ <button onclick="copyUrl('${short.url}')" class="px-3 py-2 bg-white/10 hover:bg-white/20 rounded transition">
572
+ <i class="fas fa-share-alt text-xs"></i>
573
+ </button>
574
+ </div>
575
+ </div>
576
+ `;
577
+
578
+ container.appendChild(card);
579
+ });
580
+
581
+ // Scroll to results
582
+ resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' });
583
+ }
584
+
585
+ function copyUrl(url) {
586
+ navigator.clipboard.writeText(url).then(() => {
587
+ showToast('URL copiada al portapapeles');
588
+ });
589
+ }
590
+
591
+ function downloadShort(id) {
592
+ showToast('Iniciando descarga... (Simulado)');
593
+ setTimeout(() => {
594
+ showToast('Descarga completada');
595
+ }, 2000);
596
+ }
597
+
598
+ function exportJSON() {
599
+ const shorts = Array.from(document.querySelectorAll('.glass-card')).map(card => ({
600
+ title: card.querySelector('h4').textContent,
601
+ url: 'https://youtube.com/shorts/demo'
602
+ }));
603
+
604
+ const dataStr = JSON.stringify(shorts, null, 2);
605
+ const blob = new Blob([dataStr], { type: 'application/json' });
606
+ const url = URL.createObjectURL(blob);
607
+ const a = document.createElement('a');
608
+ a.href = url;
609
+ a.download = 'shorts-scraped.json';
610
+ a.click();
611
+ showToast('JSON exportado correctamente');
612
+ }
613
+
614
+ function clearResults() {
615
+ document.getElementById('resultsSection').classList.add('hidden');
616
+ document.getElementById('shortsContainer').innerHTML = '';
617
+ document.getElementById('channelInput').value = '';
618
+ showToast('Resultados limpiados');
619
+ }
620
+
621
+ function copyCode() {
622
+ const code = `const puppeteer = require('puppeteer');
623
+ const fs = require('fs');
624
+
625
+ async function scrapeShorts(channelUrl, limit = 25) {
626
+ const browser = await puppeteer.launch({ headless: true });
627
+ const page = await browser.newPage();
628
+
629
+ await page.goto(\`\${channelUrl}/shorts\`, {
630
+ waitUntil: 'networkidle2'
631
+ });
632
+
633
+ let shorts = [];
634
+
635
+ while (shorts.length < limit) {
636
+ const newShorts = await page.evaluate(() => {
637
+ const items = document.querySelectorAll('ytd-reel-item-renderer');
638
+ return Array.from(items).map(item => ({
639
+ title: item.querySelector('#video-title')?.innerText,
640
+ views: item.querySelector('.style-scope ytd-grid-video-renderer')?.innerText,
641
+ url: item.querySelector('a')?.href,
642
+ thumbnail: item.querySelector('img')?.src
643
+ }));
644
+ });
645
+
646
+ shorts = [...shorts, ...newShorts];
647
+ await page.evaluate('window.scrollTo(0, document.body.scrollHeight)');
648
+ await page.waitForTimeout(2000);
649
+ }
650
+
651
+ await browser.close();
652
+ return shorts.slice(0, limit);
653
+ }`;
654
+
655
+ navigator.clipboard.writeText(code).then(() => {
656
+ showToast('Código copiado al portapapeles');
657
+ });
658
+ }
659
+
660
+ // Enter key support
661
+ document.getElementById('channelInput').addEventListener('keypress', function(e) {
662
+ if (e.key === 'Enter') startScraping();
663
+ });
664
+ </script>
665
+ </body>
666
+ </html>