163 lines
7.5 KiB
HTML
163 lines
7.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>StreamShot</title>
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<style>
|
|
body {
|
|
background-color: #0f172a;
|
|
color: #f1f5f9;
|
|
}
|
|
.sh-card {
|
|
background: rgba(30, 41, 59, 0.7);
|
|
backdrop-filter: blur(8px);
|
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="min-h-screen flex flex-col items-center justify-center p-4">
|
|
<div class="max-w-2xl w-full space-y-8 text-center">
|
|
<header>
|
|
<h1 class="text-4xl font-extrabold tracking-tight text-white mb-2">
|
|
StreamShot
|
|
<a href="https://github.com/Space-Banane/shsf" target="_blank" class="inline-block align-middle ml-2">
|
|
<img src="https://github.com/Space-Banane/Space-Banane/blob/main/WORKS%20WITH%20SHSF%20-%20BLURPLE%20TEXT.png?raw=true" height="28" class="h-7" alt="Works with SHSF">
|
|
</a>
|
|
</h1>
|
|
<p class="text-slate-400">Capture a high-quality frame from any YouTube livestream</p>
|
|
</header>
|
|
|
|
<div class="sh-card p-6 rounded-2xl shadow-xl">
|
|
<div class="flex flex-col sm:flex-row gap-3">
|
|
<input
|
|
type="text"
|
|
id="streamUrl"
|
|
placeholder="Enter YouTube URL..."
|
|
class="flex-1 bg-slate-900 border border-slate-700 rounded-xl px-4 py-3 focus:outline-none focus:ring-2 focus:ring-blue-500 transition-all text-white placeholder-slate-500"
|
|
>
|
|
<button
|
|
onclick="captureSnapshot()"
|
|
id="snapBtn"
|
|
class="bg-blue-600 hover:bg-blue-500 text-white font-semibold py-3 px-8 rounded-xl transition-all shadow-lg active:scale-95 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
>
|
|
Capture
|
|
</button>
|
|
</div>
|
|
|
|
<div id="status" class="mt-4 text-sm font-medium hidden text-blue-400"></div>
|
|
</div>
|
|
|
|
<div id="imageContainer" class="hidden space-y-4">
|
|
<div class="sh-card p-2 rounded-2xl overflow-hidden shadow-2xl">
|
|
<img id="snapshotImg" class="w-full rounded-xl" src="" alt="Snapshot">
|
|
</div>
|
|
<div class="flex justify-center gap-4">
|
|
<a
|
|
id="downloadBtn"
|
|
href="#"
|
|
download="snapshot.jpg"
|
|
class="inline-flex items-center gap-2 bg-blue-600 hover:bg-blue-500 text-white py-2.5 px-6 rounded-xl transition-all shadow-md"
|
|
>
|
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
<path fill-rule="evenodd" d="M3 17a1 1 0 011-1h12a1 1 0 110 2H4a1 1 0 01-1-1zm3.293-7.707a1 1 0 011.414 0L9 10.586V3a1 1 0 112 0v7.586l1.293-1.293a1 1 0 111.414 1.414l-3 3a1 1 0 01-1.414 0l-3-3a1 1 0 010-1.414z" clip-rule="evenodd" />
|
|
</svg>
|
|
Download Image
|
|
</a>
|
|
</div>
|
|
</div>
|
|
|
|
<footer class="mt-12 text-left bg-slate-900/50 rounded-2xl p-6 border border-slate-800">
|
|
<h2 class="text-lg font-semibold text-white mb-4">API Documentation (for AI/CLI)</h2>
|
|
<div class="space-y-4">
|
|
<div>
|
|
<p class="text-sm text-slate-400 mb-2">To capture a snapshot and save the binary image directly using <code class="bg-slate-800 px-1.5 py-0.5 rounded text-blue-300">curl</code> and <code class="bg-slate-800 px-1.5 py-0.5 rounded text-blue-300">jq</code>:</p>
|
|
<pre class="bg-black/40 p-4 rounded-xl text-xs overflow-x-auto text-slate-300 border border-slate-700"><code>curl -X POST \
|
|
https://shsf-api.reversed.dev/api/exec/16/cca815a2-216d-4362-b6f5-1d57ac9f088d/snapshot \
|
|
-H "Content-Type: application/json" \
|
|
-d '{"url": "https://www.youtube.com/watch?v=STREAMID"}' \
|
|
| jq -r .buffer | base64 -d > snapshot.jpg</code></pre>
|
|
</div>
|
|
<div class="text-xs text-slate-500 italic">
|
|
Note: The response is a JSON object with a base64 encoded string in the <code class="text-slate-400">buffer</code> field.
|
|
</div>
|
|
</div>
|
|
</footer>
|
|
</div>
|
|
|
|
<script>
|
|
const API_BASE = 'https://shsf-api.reversed.dev/api/exec/16/cca815a2-216d-4362-b6f5-1d57ac9f088d';
|
|
|
|
async function captureSnapshot() {
|
|
const urlInput = document.getElementById('streamUrl');
|
|
const snapBtn = document.getElementById('snapBtn');
|
|
const status = document.getElementById('status');
|
|
const imgContainer = document.getElementById('imageContainer');
|
|
const snapshotImg = document.getElementById('snapshotImg');
|
|
const downloadBtn = document.getElementById('downloadBtn');
|
|
|
|
const youtubeUrl = urlInput.value.trim();
|
|
if (!youtubeUrl) {
|
|
status.textContent = 'Please enter a valid YouTube URL';
|
|
status.classList.remove('hidden', 'text-blue-400');
|
|
status.classList.add('text-red-400');
|
|
return;
|
|
}
|
|
|
|
// Reset UI
|
|
snapBtn.disabled = true;
|
|
snapBtn.textContent = 'Capturing...';
|
|
status.textContent = 'Extracting stream and capturing frame...';
|
|
status.classList.remove('hidden', 'text-red-400');
|
|
status.classList.add('text-blue-400');
|
|
imgContainer.classList.add('hidden');
|
|
|
|
try {
|
|
// The API expects a POST to /snapshot with a JSON body { "url": "..." }
|
|
const response = await fetch(API_BASE + '/snapshot', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ url: youtubeUrl }),
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorData = await response.json().catch(() => ({}));
|
|
throw new Error(errorData.message || `Server error: ${response.status}`);
|
|
}
|
|
|
|
const data = await response.json();
|
|
const binaryString = atob(data.buffer);
|
|
const bytes = new Uint8Array(binaryString.length);
|
|
for (let i = 0; i < binaryString.length; i++) {
|
|
bytes[i] = binaryString.charCodeAt(i);
|
|
}
|
|
const blob = new Blob([bytes], { type: 'image/jpeg' });
|
|
const objectUrl = URL.createObjectURL(blob);
|
|
|
|
snapshotImg.src = objectUrl;
|
|
downloadBtn.href = objectUrl;
|
|
|
|
imgContainer.classList.remove('hidden');
|
|
status.classList.add('hidden');
|
|
} catch (err) {
|
|
console.error(err);
|
|
status.textContent = `Error: ${err.message}`;
|
|
status.classList.remove('text-blue-400');
|
|
status.classList.add('text-red-400');
|
|
} finally {
|
|
snapBtn.disabled = false;
|
|
snapBtn.textContent = 'Capture';
|
|
}
|
|
}
|
|
|
|
// Add Enter key support
|
|
document.getElementById('streamUrl').addEventListener('keypress', (e) => {
|
|
if (e.key === 'Enter') captureSnapshot();
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|