anycoder-a7737669 / index.html
alfabill's picture
Upload folder using huggingface_hub
f4e73a4 verified
<!DOCTYPE html>
<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>