Spaces:
Runtime error
Runtime error
File size: 12,147 Bytes
029016a |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 |
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>InstaSpy - Bulk Scraper SaaS</title>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/lucide@latest"></script>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
<script>
tailwind.config = {
theme: {
extend: {
fontFamily: { sans: ['Inter', 'sans-serif'] },
animation: { 'fade-in': 'fadeIn 0.5s ease-out forwards' },
keyframes: {
fadeIn: {
'0%': { opacity: '0', transform: 'translateY(10px)' },
'100%': { opacity: '1', transform: 'translateY(0)' },
}
}
}
}
}
</script>
<style>
body { background-color: #020617; color: #e2e8f0; }
.glass-panel {
background: rgba(15, 23, 42, 0.6);
backdrop-filter: blur(12px);
border: 1px solid rgba(148, 163, 184, 0.1);
}
.gradient-text {
background: linear-gradient(135deg, #f472b6 0%, #a855f7 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
</style>
</head>
<body class="min-h-screen flex flex-col font-sans selection:bg-pink-500 selection:text-white">
<nav class="border-b border-slate-800 bg-slate-950/80 sticky top-0 z-50 backdrop-blur-md">
<div class="max-w-7xl mx-auto px-6 h-16 flex items-center justify-between">
<div class="flex items-center gap-2">
<div class="bg-gradient-to-br from-pink-500 to-purple-600 p-1.5 rounded-lg shadow-lg shadow-purple-500/20">
<i data-lucide="instagram" class="w-5 h-5 text-white"></i>
</div>
<span class="font-bold text-xl tracking-tight text-white">InstaSpy<span class="text-pink-500">.io</span></span>
</div>
</div>
</nav>
<main class="flex-grow max-w-7xl mx-auto px-6 py-12 w-full space-y-12">
<div class="text-center space-y-4 animate-fade-in">
<h1 class="text-4xl md:text-5xl font-extrabold tracking-tight text-white">
Instagram <span class="gradient-text">Bulk Data Scraper</span>
</h1>
<p class="text-slate-400 max-w-2xl mx-auto text-lg leading-relaxed">
Extract <span class="text-white font-medium">Views, Likes, and Followers</span> from Reels, Posts, and Profiles instantly.
</p>
</div>
<div class="glass-panel rounded-2xl p-1.5 shadow-2xl shadow-purple-900/10 animate-fade-in" style="animation-delay: 0.1s;">
<div class="bg-[#0f172a] rounded-xl p-4">
<div class="flex justify-between mb-2">
<label class="text-xs font-semibold uppercase tracking-wider text-slate-500">Target URLs</label>
<button class="text-xs text-pink-400 hover:text-pink-300 transition" onclick="pasteExample()">Paste Example Links</button>
</div>
<textarea id="urlInput" placeholder="Paste Instagram links here (separated by commas or new lines)..." class="w-full h-48 bg-transparent text-slate-300 placeholder:text-slate-600 focus:outline-none resize-none font-mono text-sm leading-6 custom-scrollbar"></textarea>
</div>
<div class="flex justify-between items-center px-4 py-3 bg-slate-900/50 rounded-b-xl border-t border-slate-800">
<span id="linkCount" class="text-xs text-slate-500 font-mono">0 Links detected</span>
<button id="scrapeBtn" onclick="startScrape()" class="flex items-center gap-2 px-6 py-2.5 rounded-lg font-semibold text-sm transition-all bg-gradient-to-r from-pink-600 to-purple-600 hover:from-pink-500 hover:to-purple-500 text-white shadow-lg shadow-pink-900/20 active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed">
<i data-lucide="play" class="w-4 h-4 fill-current"></i> Start Extraction
</button>
</div>
</div>
<div id="resultsArea" class="hidden space-y-6 animate-fade-in">
<div class="border border-slate-800 rounded-xl overflow-hidden bg-slate-900/40 backdrop-blur-sm">
<div class="flex items-center justify-between p-4 border-b border-slate-800 bg-slate-900/60">
<h3 class="font-semibold text-white flex items-center gap-2 text-sm">
<i data-lucide="database" class="w-4 h-4 text-slate-400"></i> Extracted Data
</h3>
<button onclick="downloadCSV()" class="flex items-center gap-2 px-3 py-1.5 bg-slate-800 hover:bg-slate-700 hover:text-white text-xs font-medium rounded-lg text-slate-300 transition border border-slate-700">
<i data-lucide="download" class="w-3.5 h-3.5"></i> Export Excel
</button>
</div>
<div class="overflow-x-auto">
<table class="w-full text-sm text-left">
<thead class="text-xs text-slate-400 uppercase bg-slate-900/80 border-b border-slate-800">
<tr>
<th class="px-6 py-3 font-medium">Type</th>
<th class="px-6 py-3 font-medium">Source URL</th>
<th class="px-6 py-3 font-medium">Author</th>
<th class="px-6 py-3 font-medium text-right">Followers</th>
<th class="px-6 py-3 font-medium text-right">Likes</th>
<th class="px-6 py-3 font-medium text-right">Views</th>
<th class="px-6 py-3 font-medium text-center">Status</th>
</tr>
</thead>
<tbody id="tableBody" class="divide-y divide-slate-800/50"></tbody>
</table>
</div>
</div>
</div>
</main>
<script>
lucide.createIcons();
const urlInput = document.getElementById('urlInput');
const scrapeBtn = document.getElementById('scrapeBtn');
const resultsArea = document.getElementById('resultsArea');
const tableBody = document.getElementById('tableBody');
urlInput.addEventListener('input', () => {
const lines = urlInput.value.split('\n').filter(line => line.trim() !== '');
document.getElementById('linkCount').innerText = `${lines.length} Links detected`;
});
function pasteExample() {
urlInput.value = `https://www.instagram.com/reel/DTQEBbwEs_K/\nhttps://www.instagram.com/devs_clicks12/\nhttps://www.instagram.com/reel/DTSetANkglg/`;
urlInput.dispatchEvent(new Event('input'));
}
async function startScrape() {
const rawInput = urlInput.value.trim();
// Split by comma OR newline, then filter empty strings
const urls = rawInput.split(/[\n,]+/).map(u => u.trim()).filter(u => u !== '');
if (urls.length === 0) { alert("Please enter at least one URL"); return; }
scrapeBtn.disabled = true;
scrapeBtn.innerHTML = `<i data-lucide="loader-2" class="w-4 h-4 animate-spin"></i> Processing...`;
lucide.createIcons();
try {
// CALL PYTHON BACKEND
const response = await fetch('/api/scrape', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ urls: urls })
});
const data = await response.json();
renderTable(data);
resultsArea.classList.remove('hidden');
resultsArea.scrollIntoView({ behavior: 'smooth' });
} catch (error) {
alert("Error connecting to server. Is app.py running?");
console.error(error);
}
scrapeBtn.disabled = false;
scrapeBtn.innerHTML = `<i data-lucide="play" class="w-4 h-4 fill-current"></i> Start Extraction`;
lucide.createIcons();
}
function renderTable(data) {
tableBody.innerHTML = '';
data.forEach(row => {
const tr = document.createElement('tr');
tr.className = "hover:bg-slate-800/40 transition-colors group";
const typeColor = row.type === 'REEL' ? 'text-pink-400 bg-pink-400/10 border-pink-400/20'
: row.type === 'PROFILE' ? 'text-purple-400 bg-purple-400/10 border-purple-400/20'
: 'text-blue-400 bg-blue-400/10 border-blue-400/20';
const statusColor = row.status === 'Success' ? 'text-emerald-400 bg-emerald-400/10' : 'text-amber-400 bg-amber-400/10';
// Truncate URL for display
const shortUrl = row.url.length > 30 ? row.url.substring(0, 30) + "..." : row.url;
tr.innerHTML = `
<td class="px-6 py-4"><span class="px-2 py-1 rounded text-[10px] font-bold border uppercase ${typeColor}">${row.type}</span></td>
<td class="px-6 py-4">
<a href="${row.url}" target="_blank" class="text-xs text-slate-500 hover:text-pink-400 hover:underline transition font-mono flex items-center gap-1" title="${row.url}">
${shortUrl}
<i data-lucide="external-link" class="w-3 h-3"></i>
</a>
</td>
<td class="px-6 py-4 font-medium text-white">@${row.author || 'N/A'}</td>
<td class="px-6 py-4 text-right text-slate-300 font-mono">${row.followers}</td>
<td class="px-6 py-4 text-right text-slate-300 font-mono">${row.likes}</td>
<td class="px-6 py-4 text-right text-white font-bold font-mono">${row.views}</td>
<td class="px-6 py-4 text-center">
<span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-xs font-medium ${statusColor}">
<span class="w-1.5 h-1.5 rounded-full ${row.status === 'Success' ? 'bg-emerald-400' : 'bg-amber-400'}"></span>
${row.status}
</span>
</td>
`;
tableBody.appendChild(tr);
});
lucide.createIcons();
}
function downloadCSV() {
const rows = Array.from(document.querySelectorAll("table tr"));
const csvContent = rows.map(row => {
const cells = Array.from(row.querySelectorAll("th, td"));
return cells.map(cell => {
// Check if cell has a link (for the URL column)
const link = cell.querySelector("a");
const text = link ? link.getAttribute("href") : cell.innerText;
return `"${text.replace(/(\r\n|\n|\r)/gm, " ").trim()}"`;
}).join(",");
}).join("\n");
const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "instagram_export.csv";
link.click();
}
</script>
</body>
</html> |