/
streamlive
/
embedstream
Обзор
Документация
Войти
/
streamlive
/
embedstream
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
master
views/index.ejs
577 строк
20 KB
TwixoffStudio
Create: package.json, .gitkeep, parser.js, store.js, embed.ejs, error.ejs, index.ejs, .env.example, .gitignore, LICENSE, README.md, server.js
10 авг 2026, 11:06
Верифицирован
10 авг 2026, 11:06
6adfd59
Код
Авторство
О чём код?
<!DOCTYPE html> <html lang="ru"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1" /> <title><%= title %></title> <meta name="description" content="Любая ссылка на видео — чистый плеер без рекламы. VK, YouTube, Rutube, TikTok, Dropbox и десятки других. Обложка, название, описание и теги для Streamlabs / OBS." /> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Unbounded:wght@500;700&display=swap" rel="stylesheet"> <style> :root { --background: oklch(16% .012 250); --foreground: oklch(97% .005 250); --card: oklch(20% .014 250); --primary: oklch(82% .17 176); --primary-foreground: oklch(17% .03 190); --primary-glow: oklch(90% .15 190); --muted: oklch(24% .014 250); --muted-foreground: oklch(68% .014 250); --border: oklch(30% .014 250); --radius: 0.75rem; --gradient-hero: radial-gradient(120% 90% at 15% -10%, color-mix(in oklab, var(--primary) 26%, transparent), transparent 60%), radial-gradient(90% 70% at 100% 0%, color-mix(in oklab, var(--primary-glow) 14%, transparent), transparent 55%); --shadow-soft: 0 24px 60px -30px color-mix(in oklab, var(--primary) 45%, transparent); --font-display: "Unbounded", "Inter", system-ui, sans-serif; --font-sans: "Inter", system-ui, sans-serif; --danger: oklch(65% .2 25); } * { box-sizing: border-box; margin: 0; padding: 0; } html { scroll-behavior: smooth; } body { font-family: var(--font-sans); background: var(--background); color: var(--foreground); min-height: 100vh; line-height: 1.5; -webkit-font-smoothing: antialiased; } a { color: inherit; text-decoration: none; } .mx-auto { margin-left: auto; margin-right: auto; } .max-w-3xl { max-width: 48rem; } .max-w-4xl { max-width: 56rem; } .px-5 { padding-left: 1.25rem; padding-right: 1.25rem; } .text-center { text-align: center; } /* header */ .header { display: flex; align-items: center; justify-content: space-between; padding: 1.5rem 0; } .logo { font-family: var(--font-display); font-weight: 700; font-size: 1.25rem; letter-spacing: -0.02em; } .logo .accent { color: var(--primary); } .header-link { font-size: 0.875rem; color: var(--muted-foreground); transition: color .15s; } .header-link:hover { color: var(--primary); } /* hero */ .hero { position: relative; overflow: hidden; background-image: var(--gradient-hero); padding: 2.5rem 0 3.5rem; } .hero-grid { pointer-events: none; position: absolute; inset: 0; background-image: linear-gradient(to right, color-mix(in oklab, var(--border) 60%, transparent) 1px, transparent 1px), linear-gradient(to bottom, color-mix(in oklab, var(--border) 60%, transparent) 1px, transparent 1px); background-size: 56px 56px; -webkit-mask-image: radial-gradient(80% 60% at 50% 0%, #000, transparent 75%); mask-image: radial-gradient(80% 60% at 50% 0%, #000, transparent 75%); } .hero-inner { position: relative; } h1 { font-family: var(--font-display); font-weight: 700; font-size: clamp(1.85rem, 5vw, 2.75rem); letter-spacing: -0.03em; line-height: 1.15; margin-bottom: 1rem; } h1 .accent { color: var(--primary); } .lead { font-size: 1.05rem; color: var(--muted-foreground); max-width: 36rem; margin: 0 auto 2rem; } /* form */ .form-card { background: var(--card); border: 1px solid var(--border); border-radius: calc(var(--radius) + 4px); padding: 1.25rem; box-shadow: var(--shadow-soft); max-width: 40rem; margin: 0 auto; } .input-row { display: flex; gap: 0.6rem; flex-wrap: wrap; } .url-input { flex: 1; min-width: 200px; background: var(--background); border: 1px solid var(--border); border-radius: var(--radius); color: var(--foreground); font-size: 0.95rem; padding: 0.85rem 1rem; outline: none; transition: border-color .15s; } .url-input:focus { border-color: color-mix(in oklab, var(--primary) 55%, var(--border)); } .url-input::placeholder { color: var(--muted-foreground); } .btn { background: var(--primary); color: var(--primary-foreground); font-weight: 600; font-size: 0.95rem; border: none; border-radius: var(--radius); padding: 0.85rem 1.4rem; cursor: pointer; transition: transform .12s, opacity .15s, box-shadow .15s; white-space: nowrap; } .btn:hover { transform: translateY(-1px); box-shadow: 0 8px 24px -8px color-mix(in oklab, var(--primary) 50%, transparent); } .btn:active { transform: translateY(0); } .btn:disabled { opacity: 0.55; cursor: not-allowed; transform: none; } .btn-ghost { background: transparent; color: var(--muted-foreground); border: 1px solid var(--border); } .btn-ghost:hover { color: var(--primary); border-color: color-mix(in oklab, var(--primary) 40%, var(--border)); box-shadow: none; } /* platforms marquee */ .marquee-wrap { overflow: hidden; margin-top: 2.5rem; border-top: 1px solid color-mix(in oklab, var(--border) 50%, transparent); border-bottom: 1px solid color-mix(in oklab, var(--border) 50%, transparent); background: color-mix(in oklab, var(--card) 40%, transparent); mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); -webkit-mask-image: linear-gradient(90deg, transparent, #000 8%, #000 92%, transparent); } .marquee { display: flex; gap: 0.5rem; width: max-content; padding: 0.85rem 0; animation: marquee 40s linear infinite; } .marquee span { flex-shrink: 0; font-size: 0.75rem; padding: 0.3rem 0.75rem; border-radius: 0.5rem; border: 1px solid var(--border); background: var(--card); color: var(--muted-foreground); white-space: nowrap; } @keyframes marquee { from { transform: translateX(0); } to { transform: translateX(-50%); } } /* steps */ .section { padding: 3.5rem 0; } .section-title { font-family: var(--font-display); font-weight: 700; font-size: 1.5rem; letter-spacing: -0.02em; margin-bottom: 1.75rem; text-align: center; } .steps { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 1.25rem; } .step { background: var(--card); border: 1px solid var(--border); border-radius: calc(var(--radius) + 2px); padding: 1.35rem; } .step-num { font-family: var(--font-display); font-weight: 700; font-size: 0.8rem; color: var(--primary); margin-bottom: 0.5rem; } .step h3 { font-size: 1.05rem; font-weight: 600; margin-bottom: 0.4rem; } .step p { font-size: 0.875rem; color: var(--muted-foreground); } /* result */ .result { display: none; margin-top: 1.5rem; background: var(--card); border: 1px solid var(--border); border-radius: calc(var(--radius) + 4px); overflow: hidden; } .result.show { display: block; } .result-preview { display: grid; grid-template-columns: 200px 1fr; gap: 0; } @media (max-width: 600px) { .result-preview { grid-template-columns: 1fr; } } .result-thumb { background: var(--muted); aspect-ratio: 16/9; object-fit: cover; width: 100%; height: 100%; min-height: 120px; } .result-body { padding: 1.1rem 1.25rem; } .result-platform { font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.06em; color: var(--primary); font-weight: 600; margin-bottom: 0.35rem; } .result-title { font-weight: 600; font-size: 1.05rem; margin-bottom: 0.35rem; line-height: 1.3; } .result-desc { font-size: 0.85rem; color: var(--muted-foreground); margin-bottom: 0.75rem; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; } .result-tags { display: flex; flex-wrap: wrap; gap: 0.35rem; margin-bottom: 0.9rem; } .tag { font-size: 0.7rem; padding: 0.2rem 0.5rem; border-radius: 999px; background: color-mix(in oklab, var(--primary) 12%, var(--muted)); color: var(--primary); } .result-actions { display: flex; flex-wrap: wrap; gap: 0.5rem; align-items: center; } .copy-field { flex: 1; min-width: 180px; background: var(--background); border: 1px solid var(--border); border-radius: var(--radius); padding: 0.55rem 0.75rem; font-size: 0.8rem; color: var(--muted-foreground); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .badge-restricted { display: inline-block; font-size: 0.7rem; padding: 0.2rem 0.55rem; border-radius: 999px; background: color-mix(in oklab, var(--danger) 18%, transparent); color: var(--danger); margin-bottom: 0.5rem; } /* features */ .features { display: grid; grid-template-columns: repeat(auto-fit, minmax(260px, 1fr)); gap: 1rem; } .feature { background: var(--card); border: 1px solid var(--border); border-radius: var(--radius); padding: 1.25rem; } .feature h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.35rem; } .feature p { font-size: 0.85rem; color: var(--muted-foreground); } /* footer */ footer { padding: 2rem 0 2.5rem; border-top: 1px solid color-mix(in oklab, var(--border) 50%, transparent); text-align: center; font-size: 0.8rem; color: var(--muted-foreground); } footer a { color: var(--primary); } footer a:hover { text-decoration: underline; } .status { margin-top: 0.75rem; font-size: 0.85rem; color: var(--muted-foreground); min-height: 1.25rem; } .status.error { color: var(--danger); } .status.ok { color: var(--primary); } .loader { display: inline-block; width: 1em; height: 1em; border: 2px solid color-mix(in oklab, var(--primary) 30%, transparent); border-top-color: var(--primary); border-radius: 50%; animation: spin .6s linear infinite; vertical-align: -2px; margin-right: 0.4rem; } @keyframes spin { to { transform: rotate(360deg); } } </style> </head> <body> <div class="hero"> <div class="hero-grid"></div> <div class="hero-inner mx-auto max-w-4xl px-5"> <header class="header"> <div class="logo">Embed<span class="accent">Stream</span></div> <a class="header-link" href="https://gitverse.ru" target="_blank" rel="noopener">Gitverse</a> </header> <div class="text-center"> <h1>Любая ссылка на видео —<br><span class="accent">чистый плеер</span> без рекламы</h1> <p class="lead"> Instagram, VK, Rutube, TikTok, Dropbox, YouTube и десятки других платформ. Мы вытягиваем обложку, название, описание и теги — и отдаём одну короткую ссылку для встраивания в Streamlabs / OBS. </p> </div> <div class="form-card"> <form id="parse-form"> <div class="input-row"> <input class="url-input" type="url" id="url" name="url" placeholder="https://vk.com/video... или любая другая ссылка" required autocomplete="off" spellcheck="false" /> <button class="btn" type="submit" id="submit-btn">Сделать Embed</button> </div> </form> <div class="status" id="status"></div> <div class="result" id="result"> <div class="result-preview"> <img class="result-thumb" id="res-image" alt="" /> <div class="result-body"> <div id="res-restricted" class="badge-restricted" style="display:none">Запрещена в РФ</div> <div class="result-platform" id="res-platform"></div> <div class="result-title" id="res-title"></div> <div class="result-desc" id="res-desc"></div> <div class="result-tags" id="res-tags"></div> <div class="result-actions"> <div class="copy-field" id="res-url" title="Нажмите чтобы скопировать"></div> <button class="btn" type="button" id="copy-btn">Копировать</button> <a class="btn btn-ghost" id="open-btn" target="_blank" rel="noopener">Открыть</a> </div> </div> </div> </div> </div> <div class="marquee-wrap"> <div class="marquee" id="marquee"> <!-- filled by JS --> </div> </div> </div> </div> <section class="section"> <div class="mx-auto max-w-4xl px-5"> <h2 class="section-title">Как это работает</h2> <div class="steps"> <div class="step"> <div class="step-num">01</div> <h3>Вставляете ссылку</h3> <p>Любая платформа: соцсети, видеохостинги, облака, прямые mp4 и HLS-потоки.</p> </div> <div class="step"> <div class="step-num">02</div> <h3>Мы чистим и парсим</h3> <p>Достаём обложку (настоящую, не скриншот), название, описание и теги через Open Graph и oEmbed.</p> </div> <div class="step"> <div class="step-num">03</div> <h3>Встраиваете на стрим</h3> <p>Получаете короткую ссылку /embed/xxxxxxxx и готовый iframe для browser source в Streamlabs / OBS.</p> </div> </div> </div> </section> <section class="section" style="padding-top:0"> <div class="mx-auto max-w-4xl px-5"> <div class="features"> <div class="feature"> <h3>Работает даже если платформа недоступна</h3> <p>Метаданные забираются надёжно. Плеер и обложка собираются в любом случае.</p> </div> <div class="feature"> <h3>Маркировка по закону РФ</h3> <p>Контент с платформ, деятельность которых ограничена в РФ, автоматически помечается плашкой «Запрещена в РФ».</p> </div> <div class="feature"> <h3>Готово для Streamlabs / OBS</h3> <p>Добавляйте ссылку как Browser Source — подтянутся название, описание, теги и обложка.</p> </div> </div> </div> </section> <footer> <div class="mx-auto max-w-4xl px-5"> EmbedStream · опенсорс · MIT License<br> Сделано для стримеров · <a href="https://gitverse.ru" target="_blank" rel="noopener">Gitverse</a> </div> </footer> <script> const platforms = [ 'YouTube', 'VK Видео', 'Rutube', 'Instagram', 'TikTok', 'Dropbox', 'Vimeo', 'Одноклассники', 'Twitch', 'Telegram', 'Google Drive', 'Яндекс Диск', 'Dailymotion', 'SoundCloud', 'MP4 / HLS' ]; const marquee = document.getElementById('marquee'); const items = [...platforms, ...platforms].map(p => `<span>${p}</span>`).join(''); marquee.innerHTML = items; const form = document.getElementById('parse-form'); const statusEl = document.getElementById('status'); const resultEl = document.getElementById('result'); const submitBtn = document.getElementById('submit-btn'); form.addEventListener('submit', async (e) => { e.preventDefault(); const url = document.getElementById('url').value.trim(); if (!url) return; statusEl.className = 'status'; statusEl.innerHTML = '<span class="loader"></span> Парсим ссылку…'; resultEl.classList.remove('show'); submitBtn.disabled = true; try { const res = await fetch('/api/parse', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || 'Ошибка сервера'); // fill result document.getElementById('res-platform').textContent = data.meta.platform || 'generic'; document.getElementById('res-title').textContent = data.meta.title || 'Без названия'; document.getElementById('res-desc').textContent = data.meta.description || ''; document.getElementById('res-url').textContent = data.embedUrl; document.getElementById('res-url').dataset.url = data.embedUrl; document.getElementById('open-btn').href = data.embedUrl; const img = document.getElementById('res-image'); if (data.meta.image) { img.src = data.meta.image; img.style.display = ''; } else { img.removeAttribute('src'); img.style.display = 'none'; } const tagsEl = document.getElementById('res-tags'); tagsEl.innerHTML = (data.meta.tags || []).map(t => `<span class="tag">${t}</span>`).join(''); const restr = document.getElementById('res-restricted'); restr.style.display = data.meta.restricted ? 'inline-block' : 'none'; resultEl.classList.add('show'); statusEl.className = 'status ok'; statusEl.textContent = 'Готово! Скопируйте ссылку и добавьте как Browser Source.'; } catch (err) { statusEl.className = 'status error'; statusEl.textContent = err.message || 'Не удалось обработать ссылку'; } finally { submitBtn.disabled = false; } }); document.getElementById('copy-btn').addEventListener('click', async () => { const url = document.getElementById('res-url').dataset.url; try { await navigator.clipboard.writeText(url); const btn = document.getElementById('copy-btn'); const old = btn.textContent; btn.textContent = 'Скопировано!'; setTimeout(() => btn.textContent = old, 1600); } catch { // fallback const range = document.createRange(); range.selectNode(document.getElementById('res-url')); window.getSelection().removeAllRanges(); window.getSelection().addRange(range); document.execCommand('copy'); } }); document.getElementById('res-url').addEventListener('click', () => { document.getElementById('copy-btn').click(); }); </script> </body> </html>