Spaces:
Running
Running
| <html lang="es"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>YouTube Shorts Scraper Pro</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" rel="stylesheet"> | |
| <link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;600;700&family=JetBrains+Mono:wght@400;700&display=swap" rel="stylesheet"> | |
| <style> | |
| :root { | |
| --primary: #ff0000; | |
| --secondary: #282828; | |
| --accent: #00d4ff; | |
| --bg-dark: #0f0f0f; | |
| --card-bg: rgba(255, 255, 255, 0.05); | |
| } | |
| * { | |
| margin: 0; | |
| padding: 0; | |
| box-sizing: border-box; | |
| } | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background: var(--bg-dark); | |
| color: white; | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| /* Animated Background */ | |
| .bg-gradient { | |
| position: fixed; | |
| top: 0; | |
| left: 0; | |
| width: 100%; | |
| height: 100%; | |
| background: | |
| radial-gradient(circle at 20% 50%, rgba(255, 0, 0, 0.15) 0%, transparent 50%), | |
| radial-gradient(circle at 80% 80%, rgba(0, 212, 255, 0.1) 0%, transparent 50%), | |
| radial-gradient(circle at 40% 20%, rgba(255, 0, 0, 0.1) 0%, transparent 50%); | |
| z-index: -1; | |
| animation: pulse 15s ease-in-out infinite; | |
| } | |
| @keyframes pulse { | |
| 0%, 100% { opacity: 0.5; transform: scale(1); } | |
| 50% { opacity: 0.8; transform: scale(1.1); } | |
| } | |
| /* Glassmorphism */ | |
| .glass { | |
| background: var(--card-bg); | |
| backdrop-filter: blur(12px); | |
| -webkit-backdrop-filter: blur(12px); | |
| border: 1px solid rgba(255, 255, 255, 0.1); | |
| } | |
| .glass-card { | |
| background: linear-gradient(135deg, rgba(255,255,255,0.1) 0%, rgba(255,255,255,0.05) 100%); | |
| backdrop-filter: blur(10px); | |
| border: 1px solid rgba(255,255,255,0.1); | |
| transition: all 0.3s ease; | |
| } | |
| .glass-card:hover { | |
| transform: translateY(-5px); | |
| border-color: rgba(255, 0, 0, 0.5); | |
| box-shadow: 0 10px 30px rgba(255, 0, 0, 0.2); | |
| } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { | |
| width: 8px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg-dark); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: #333; | |
| border-radius: 4px; | |
| } | |
| ::-webkit-scrollbar-thumb:hover { | |
| background: var(--primary); | |
| } | |
| /* Loading Animation */ | |
| .loader { | |
| width: 48px; | |
| height: 48px; | |
| border: 3px solid #FFF; | |
| border-radius: 50%; | |
| display: inline-block; | |
| position: relative; | |
| box-sizing: border-box; | |
| animation: rotation 1s linear infinite; | |
| } | |
| .loader::after { | |
| content: ''; | |
| box-sizing: border-box; | |
| position: absolute; | |
| left: 50%; | |
| top: 50%; | |
| transform: translate(-50%, -50%); | |
| width: 40px; | |
| height: 40px; | |
| border-radius: 50%; | |
| border: 3px solid transparent; | |
| border-bottom-color: #ff0000; | |
| } | |
| @keyframes rotation { | |
| 0% { transform: rotate(0deg); } | |
| 100% { transform: rotate(360deg); } | |
| } | |
| /* Code Block Styling */ | |
| .code-block { | |
| font-family: 'JetBrains Mono', monospace; | |
| background: #1e1e1e; | |
| border-radius: 8px; | |
| padding: 1.5rem; | |
| overflow-x: auto; | |
| position: relative; | |
| } | |
| .code-header { | |
| display: flex; | |
| justify-content: space-between; | |
| align-items: center; | |
| margin-bottom: 1rem; | |
| padding-bottom: 1rem; | |
| border-bottom: 1px solid #333; | |
| } | |
| .syntax-keyword { color: #ff79c6; } | |
| .syntax-string { color: #f1fa8c; } | |
| .syntax-function { color: #50fa7b; } | |
| .syntax-comment { color: #6272a4; } | |
| /* Input Styling */ | |
| .custom-input { | |
| background: rgba(255,255,255,0.05); | |
| border: 2px solid rgba(255,255,255,0.1); | |
| transition: all 0.3s; | |
| } | |
| .custom-input:focus { | |
| outline: none; | |
| border-color: var(--primary); | |
| background: rgba(255,255,255,0.1); | |
| } | |
| /* Grid Animation */ | |
| @keyframes fadeInUp { | |
| from { | |
| opacity: 0; | |
| transform: translateY(20px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| .animate-in { | |
| animation: fadeInUp 0.5s ease forwards; | |
| } | |
| /* Responsive Grid */ | |
| .shorts-grid { | |
| display: grid; | |
| grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); | |
| gap: 1.5rem; | |
| } | |
| @media (max-width: 640px) { | |
| .shorts-grid { | |
| grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); | |
| gap: 1rem; | |
| } | |
| } | |
| /* AnyCoder Link */ | |
| .anycoder-link { | |
| background: linear-gradient(90deg, #ff0000, #ff6b6b); | |
| -webkit-background-clip: text; | |
| -webkit-text-fill-color: transparent; | |
| font-weight: 700; | |
| text-decoration: none; | |
| position: relative; | |
| } | |
| .anycoder-link::after { | |
| content: ''; | |
| position: absolute; | |
| bottom: -2px; | |
| left: 0; | |
| width: 0; | |
| height: 2px; | |
| background: linear-gradient(90deg, #ff0000, #ff6b6b); | |
| transition: width 0.3s; | |
| } | |
| .anycoder-link:hover::after { | |
| width: 100%; | |
| } | |
| /* Toast Notification */ | |
| .toast { | |
| position: fixed; | |
| bottom: 20px; | |
| right: 20px; | |
| background: rgba(0,0,0,0.9); | |
| border-left: 4px solid var(--primary); | |
| padding: 1rem 2rem; | |
| border-radius: 4px; | |
| transform: translateX(400px); | |
| transition: transform 0.3s ease; | |
| z-index: 1000; | |
| } | |
| .toast.show { | |
| transform: translateX(0); | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="bg-gradient"></div> | |
| <!-- Header --> | |
| <header class="glass sticky top-0 z-50 border-b border-white/10"> | |
| <div class="container mx-auto px-4 py-4 flex justify-between items-center"> | |
| <div class="flex items-center gap-3"> | |
| <div class="w-10 h-10 bg-red-600 rounded-lg flex items-center justify-center"> | |
| <i class="fab fa-youtube text-white text-xl"></i> | |
| </div> | |
| <div> | |
| <h1 class="text-xl font-bold tracking-tight">Shorts Scraper <span class="text-red-500">Pro</span></h1> | |
| <p class="text-xs text-gray-400">Node.js Powered</p> | |
| </div> | |
| </div> | |
| <a href="https://huggingface.co/spaces/akhaliq/anycoder" target="_blank" class="anycoder-link text-sm"> | |
| Built with anycoder <i class="fas fa-external-link-alt ml-1 text-xs"></i> | |
| </a> | |
| </div> | |
| </header> | |
| <main class="container mx-auto px-4 py-8 max-w-7xl"> | |
| <!-- Hero Section --> | |
| <section class="text-center mb-12"> | |
| <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"> | |
| Extrae Shorts de YouTube | |
| </h2> | |
| <p class="text-gray-400 text-lg max-w-2xl mx-auto mb-8"> | |
| Simulación de interfaz para scraping de contenido. En un entorno real, esto requeriría un backend Node.js con Puppeteer o yt-dlp. | |
| </p> | |
| </section> | |
| <!-- Control Panel --> | |
| <section class="glass rounded-2xl p-6 mb-12 max-w-4xl mx-auto"> | |
| <div class="grid md:grid-cols-3 gap-4 mb-6"> | |
| <div class="md:col-span-2"> | |
| <label class="block text-sm font-medium mb-2 text-gray-300">URL del Canal o Búsqueda</label> | |
| <div class="relative"> | |
| <i class="fas fa-link absolute left-3 top-1/2 -translate-y-1/2 text-gray-500"></i> | |
| <input type="text" id="channelInput" placeholder="https://youtube.com/@channel o término de búsqueda" | |
| class="custom-input w-full pl-10 pr-4 py-3 rounded-lg text-white placeholder-gray-500"> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium mb-2 text-gray-300">Límite de Shorts</label> | |
| <select id="limitSelect" class="custom-input w-full px-4 py-3 rounded-lg text-white cursor-pointer"> | |
| <option value="10">10 shorts</option> | |
| <option value="25" selected>25 shorts</option> | |
| <option value="50">50 shorts</option> | |
| <option value="100">100 shorts</option> | |
| </select> | |
| </div> | |
| </div> | |
| <div class="flex flex-wrap gap-3 mb-6"> | |
| <button onclick="toggleAdvanced()" class="text-sm text-gray-400 hover:text-white transition flex items-center gap-2"> | |
| <i class="fas fa-cog"></i> Opciones Avanzadas | |
| </button> | |
| </div> | |
| <div id="advancedOptions" class="hidden mb-6 p-4 bg-black/20 rounded-lg border border-white/5"> | |
| <div class="grid md:grid-cols-3 gap-4"> | |
| <label class="flex items-center gap-2 cursor-pointer"> | |
| <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"> | |
| <span class="text-sm text-gray-300">Incluir Metadata</span> | |
| </label> | |
| <label class="flex items-center gap-2 cursor-pointer"> | |
| <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"> | |
| <span class="text-sm text-gray-300">Alta Calidad</span> | |
| </label> | |
| <label class="flex items-center gap-2 cursor-pointer"> | |
| <input type="checkbox" id="autoDownload" class="w-4 h-4 rounded border-gray-600 text-red-600 focus:ring-red-600 bg-gray-700"> | |
| <span class="text-sm text-gray-300">Auto-descargar</span> | |
| </label> | |
| </div> | |
| </div> | |
| <button onclick="startScraping()" id="scrapeBtn" | |
| 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"> | |
| <i class="fas fa-play"></i> | |
| Iniciar Scraping | |
| </button> | |
| <!-- Progress Bar --> | |
| <div id="progressContainer" class="hidden mt-6"> | |
| <div class="flex justify-between text-sm mb-2 text-gray-400"> | |
| <span id="progressText">Iniciando navegador...</span> | |
| <span id="progressPercent">0%</span> | |
| </div> | |
| <div class="w-full bg-gray-700 rounded-full h-2 overflow-hidden"> | |
| <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> | |
| </div> | |
| </div> | |
| </section> | |
| <!-- Results Section --> | |
| <section id="resultsSection" class="hidden"> | |
| <div class="flex justify-between items-center mb-6"> | |
| <h3 class="text-2xl font-bold flex items-center gap-2"> | |
| <i class="fas fa-film text-red-500"></i> | |
| Resultados | |
| <span id="resultCount" class="text-sm bg-red-600 px-2 py-1 rounded-full">0</span> | |
| </h3> | |
| <div class="flex gap-2"> | |
| <button onclick="exportJSON()" class="glass px-4 py-2 rounded-lg text-sm hover:bg-white/10 transition flex items-center gap-2"> | |
| <i class="fas fa-download"></i> Exportar JSON | |
| </button> | |
| <button onclick="clearResults()" class="glass px-4 py-2 rounded-lg text-sm hover:bg-white/10 transition text-red-400"> | |
| <i class="fas fa-trash"></i> Limpiar | |
| </button> | |
| </div> | |
| </div> | |
| <div id="shortsContainer" class="shorts-grid"> | |
| <!-- Shorts will be injected here --> | |
| </div> | |
| </section> | |
| <!-- Node.js Code Reference --> | |
| <section class="mt-16 mb-12"> | |
| <div class="glass rounded-2xl p-8"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <div> | |
| <h3 class="text-2xl font-bold mb-2">Implementación Node.js Real</h3> | |
| <p class="text-gray-400 text-sm">Código necesario para ejecutar esto en un servidor real</p> | |
| </div> | |
| <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"> | |
| <i class="fas fa-copy"></i> Copiar | |
| </button> | |
| </div> | |
| <div class="code-block text-sm"> | |
| <div class="code-header"> | |
| <span class="text-gray-400">scraper.js</span> | |
| <div class="flex gap-2"> | |
| <div class="w-3 h-3 rounded-full bg-red-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-yellow-500"></div> | |
| <div class="w-3 h-3 rounded-full bg-green-500"></div> | |
| </div> | |
| </div> | |
| <pre><code><span class="syntax-keyword">const</span> puppeteer = <span class="syntax-function">require</span>(<span class="syntax-string">'puppeteer'</span>); | |
| <span class="syntax-keyword">const</span> fs = <span class="syntax-function">require</span>(<span class="syntax-string">'fs'</span>); | |
| <span class="syntax-keyword">async function</span> <span class="syntax-function">scrapeShorts</span>(channelUrl, limit = <span class="syntax-string">25</span>) { | |
| <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> }); | |
| <span class="syntax-keyword">const</span> page = <span class="syntax-keyword">await</span> browser.<span class="syntax-function">newPage</span>(); | |
| <span class="syntax-comment">// Navegar al canal</span> | |
| <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>, { | |
| waitUntil: <span class="syntax-string">'networkidle2'</span> | |
| }); | |
| <span class="syntax-comment">// Scroll infinito para cargar shorts</span> | |
| <span class="syntax-keyword">let</span> shorts = []; | |
| <span class="syntax-keyword">let</span> previousHeight = <span class="syntax-string">0</span>; | |
| <span class="syntax-keyword">while</span> (shorts.length < limit) { | |
| <span class="syntax-keyword">const</span> newShorts = <span class="syntax-keyword">await</span> page.<span class="syntax-function">evaluate</span>(() => { | |
| <span class="syntax-keyword">const</span> items = document.<span class="syntax-function">querySelectorAll</span>(<span class="syntax-string">'ytd-reel-item-renderer'</span>); | |
| <span class="syntax-keyword">return</span> Array.<span class="syntax-function">from</span>(items).<span class="syntax-function">map</span>(item => ({ | |
| title: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'#video-title'</span>)?.innerText, | |
| views: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string'>'.style-scope ytd-grid-video-renderer'</span>)?.innerText, | |
| url: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'a'</span>)?.href, | |
| thumbnail: item.<span class="syntax-function">querySelector</span>(<span class="syntax-string">'img'</span>)?.src | |
| })); | |
| }); | |
| shorts = [...shorts, ...newShorts]; | |
| <span class="syntax-comment">// Scroll down</span> | |
| <span class="syntax-keyword">await</span> page.<span class="syntax-function">evaluate</span>(<span class="syntax-string">'window.scrollTo(0, document.body.scrollHeight)'</span>); | |
| <span class="syntax-keyword">await</span> page.<span class="syntax-function">waitForTimeout</span>(<span class="syntax-string">2000</span>); | |
| } | |
| <span class="syntax-keyword">await</span> browser.<span class="syntax-function">close</span>(); | |
| <span class="syntax-keyword">return</span> shorts.<span class="syntax-function">slice</span>(<span class="syntax-string">0</span>, limit); | |
| } | |
| <span class="syntax-comment">// Ejecutar</span> | |
| <span class="syntax-function">scrapeShorts</span>(process.argv[<span class="syntax-string">2</span>], <span class="syntax-string">25</span>) | |
| .<span class="syntax-function">then</span>(data => { | |
| 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>)); | |
| console.<span class="syntax-function">log</span>(<span class="syntax-string">'✅ Scraping completado'</span>); | |
| });</code></pre> | |
| </div> | |
| <div class="mt-6 p-4 bg-yellow-500/10 border border-yellow-500/20 rounded-lg"> | |
| <p class="text-sm text-yellow-200 flex items-start gap-2"> | |
| <i class="fas fa-exclamation-triangle mt-1"></i> | |
| <span> | |
| <strong>Nota importante:</strong> YouTube tiene protecciones contra scraping (CORS, rate limiting, CAPTCHAs). | |
| Para uso real, se recomienda usar la API oficial de YouTube Data API v3 o yt-dlp con proxies rotativos. | |
| </span> | |
| </p> | |
| </div> | |
| </div> | |
| </section> | |
| </main> | |
| <!-- Toast Notification --> | |
| <div id="toast" class="toast"> | |
| <div class="flex items-center gap-3"> | |
| <i class="fas fa-check-circle text-green-500"></i> | |
| <span id="toastMessage">Operación completada</span> | |
| </div> | |
| </div> | |
| <script> | |
| // Mock Data Generator | |
| const mockTitles = [ | |
| "¡Increíble truco de magia! 🎩✨", | |
| "Resumen del partido ⚽🔥", | |
| "Tutorial rápido de cocina 🍳", | |
| "Reacción épica 😱", | |
| "Datos curiosos que no sabías 🤯", | |
| "Behind the scenes 🎬", | |
| "Vlog diario 📹", | |
| "Challenge accepted 💪", | |
| "Un día en mi vida 🌅", | |
| "Review honesto ⭐" | |
| ]; | |
| const mockChannels = [ | |
| "CreatorPro", "ShortsMaster", "ViralClips", "DailyDose", | |
| "TechTips", "FoodieLife", "GamingZone", "FitnessDaily" | |
| ]; | |
| function generateMockShorts(count) { | |
| const shorts = []; | |
| for (let i = 0; i < count; i++) { | |
| const id = Math.random().toString(36).substr(2, 9); | |
| const views = Math.floor(Math.random() * 999) + 1; | |
| const likes = Math.floor(Math.random() * 50) + 1; | |
| shorts.push({ | |
| id: id, | |
| title: mockTitles[Math.floor(Math.random() * mockTitles.length)], | |
| channel: mockChannels[Math.floor(Math.random() * mockChannels.length)], | |
| views: views > 900 ? `${views}K` : `${views}K`, | |
| likes: likes, | |
| duration: `${Math.floor(Math.random() * 50) + 10}s`, | |
| thumbnail: `https://picsum.photos/seed/${id}/400/700`, | |
| url: `https://youtube.com/shorts/${id}` | |
| }); | |
| } | |
| return shorts; | |
| } | |
| // UI Functions | |
| function toggleAdvanced() { | |
| const options = document.getElementById('advancedOptions'); | |
| options.classList.toggle('hidden'); | |
| } | |
| function showToast(message) { | |
| const toast = document.getElementById('toast'); | |
| document.getElementById('toastMessage').textContent = message; | |
| toast.classList.add('show'); | |
| setTimeout(() => toast.classList.remove('show'), 3000); | |
| } | |
| async function startScraping() { | |
| const input = document.getElementById('channelInput').value; | |
| if (!input) { | |
| showToast('Por favor ingresa una URL o término de búsqueda'); | |
| return; | |
| } | |
| const btn = document.getElementById('scrapeBtn'); | |
| const progressContainer = document.getElementById('progressContainer'); | |
| const progressBar = document.getElementById('progressBar'); | |
| const progressText = document.getElementById('progressText'); | |
| const progressPercent = document.getElementById('progressPercent'); | |
| const limit = parseInt(document.getElementById('limitSelect').value); | |
| // Reset UI | |
| btn.disabled = true; | |
| btn.innerHTML = '<span class="loader scale-50 mr-2"></span> Procesando...'; | |
| progressContainer.classList.remove('hidden'); | |
| document.getElementById('resultsSection').classList.add('hidden'); | |
| // Simulate progress | |
| const steps = [ | |
| { percent: 10, text: 'Iniciando navegador headless...' }, | |
| { percent: 25, text: 'Navegando al canal...' }, | |
| { percent: 40, text: 'Extrayendo URLs de shorts...' }, | |
| { percent: 60, text: 'Obteniendo metadata...' }, | |
| { percent: 80, text: 'Procesando thumbnails...' }, | |
| { percent: 100, text: 'Completado!' } | |
| ]; | |
| for (let step of steps) { | |
| await new Promise(r => setTimeout(r, 800)); | |
| progressBar.style.width = step.percent + '%'; | |
| progressText.textContent = step.text; | |
| progressPercent.textContent = step.percent + '%'; | |
| } | |
| // Generate results | |
| const shorts = generateMockShorts(limit); | |
| displayResults(shorts); | |
| // Reset button | |
| btn.disabled = false; | |
| btn.innerHTML = '<i class="fas fa-play"></i> Iniciar Scraping'; | |
| progressContainer.classList.add('hidden'); | |
| showToast(`Se encontraron ${shorts.length} shorts`); | |
| } | |
| function displayResults(shorts) { | |
| const container = document.getElementById('shortsContainer'); | |
| const resultsSection = document.getElementById('resultsSection'); | |
| const countBadge = document.getElementById('resultCount'); | |
| container.innerHTML = ''; | |
| countBadge.textContent = shorts.length; | |
| resultsSection.classList.remove('hidden'); | |
| shorts.forEach((short, index) => { | |
| const card = document.createElement('div'); | |
| card.className = 'glass-card rounded-xl overflow-hidden animate-in'; | |
| card.style.animationDelay = `${index * 0.05}s`; | |
| card.innerHTML = ` | |
| <div class="relative aspect-[9/16] bg-gray-800 overflow-hidden group"> | |
| <img src="${short.thumbnail}" alt="${short.title}" | |
| class="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" | |
| onerror="this.src='https://via.placeholder.com/400x700/222/fff?text=Short'"> | |
| <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> | |
| <span class="absolute bottom-2 right-2 bg-black/80 px-2 py-1 rounded text-xs font-mono">${short.duration}</span> | |
| <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"> | |
| <i class="fas fa-link text-xs"></i> | |
| </button> | |
| </div> | |
| <div class="p-4"> | |
| <h4 class="font-semibold text-sm mb-2 line-clamp-2 leading-tight">${short.title}</h4> | |
| <div class="flex items-center justify-between text-xs text-gray-400 mb-3"> | |
| <span class="flex items-center gap-1"> | |
| <i class="fas fa-user-circle"></i> ${short.channel} | |
| </span> | |
| <span class="flex items-center gap-1"> | |
| <i class="fas fa-eye"></i> ${short.views} | |
| </span> | |
| </div> | |
| <div class="flex gap-2"> | |
| <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"> | |
| <i class="fas fa-download"></i> Descargar | |
| </button> | |
| <button onclick="copyUrl('${short.url}')" class="px-3 py-2 bg-white/10 hover:bg-white/20 rounded transition"> | |
| <i class="fas fa-share-alt text-xs"></i> | |
| </button> | |
| </div> | |
| </div> | |
| `; | |
| container.appendChild(card); | |
| }); | |
| // Scroll to results | |
| resultsSection.scrollIntoView({ behavior: 'smooth', block: 'start' }); | |
| } | |
| function copyUrl(url) { | |
| navigator.clipboard.writeText(url).then(() => { | |
| showToast('URL copiada al portapapeles'); | |
| }); | |
| } | |
| function downloadShort(id) { | |
| showToast('Iniciando descarga... (Simulado)'); | |
| setTimeout(() => { | |
| showToast('Descarga completada'); | |
| }, 2000); | |
| } | |
| function exportJSON() { | |
| const shorts = Array.from(document.querySelectorAll('.glass-card')).map(card => ({ | |
| title: card.querySelector('h4').textContent, | |
| url: 'https://youtube.com/shorts/demo' | |
| })); | |
| const dataStr = JSON.stringify(shorts, null, 2); | |
| const blob = new Blob([dataStr], { type: 'application/json' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'shorts-scraped.json'; | |
| a.click(); | |
| showToast('JSON exportado correctamente'); | |
| } | |
| function clearResults() { | |
| document.getElementById('resultsSection').classList.add('hidden'); | |
| document.getElementById('shortsContainer').innerHTML = ''; | |
| document.getElementById('channelInput').value = ''; | |
| showToast('Resultados limpiados'); | |
| } | |
| function copyCode() { | |
| const code = `const puppeteer = require('puppeteer'); | |
| const fs = require('fs'); | |
| async function scrapeShorts(channelUrl, limit = 25) { | |
| const browser = await puppeteer.launch({ headless: true }); | |
| const page = await browser.newPage(); | |
| await page.goto(\`\${channelUrl}/shorts\`, { | |
| waitUntil: 'networkidle2' | |
| }); | |
| let shorts = []; | |
| while (shorts.length < limit) { | |
| const newShorts = await page.evaluate(() => { | |
| const items = document.querySelectorAll('ytd-reel-item-renderer'); | |
| return Array.from(items).map(item => ({ | |
| title: item.querySelector('#video-title')?.innerText, | |
| views: item.querySelector('.style-scope ytd-grid-video-renderer')?.innerText, | |
| url: item.querySelector('a')?.href, | |
| thumbnail: item.querySelector('img')?.src | |
| })); | |
| }); | |
| shorts = [...shorts, ...newShorts]; | |
| await page.evaluate('window.scrollTo(0, document.body.scrollHeight)'); | |
| await page.waitForTimeout(2000); | |
| } | |
| await browser.close(); | |
| return shorts.slice(0, limit); | |
| }`; | |
| navigator.clipboard.writeText(code).then(() => { | |
| showToast('Código copiado al portapapeles'); | |
| }); | |
| } | |
| // Enter key support | |
| document.getElementById('channelInput').addEventListener('keypress', function(e) { | |
| if (e.key === 'Enter') startScraping(); | |
| }); | |
| </script> | |
| </body> | |
| </html> |