import { useEffect, useRef, useState } from 'react'; type Mode = 'mp3' | 'mp4' | 'playlist' | 'facebook'; interface ModeDef { key: Mode; title: string; subtitle: string; icon: string; accent: string; button: string; formats: { value: string; label: string }[]; } const MODES: ModeDef[] = [ { key: 'mp3', title: 'MP3', subtitle: 'Pobierz dźwięk', icon: '♫', accent: '#a855f7', button: 'POBIERZ DŹWIĘK', formats: [ { value: '320', label: 'MP3 320 kbps' }, { value: '256', label: 'MP3 256 kbps' }, { value: '192', label: 'MP3 192 kbps' }, { value: '128', label: 'MP3 128 kbps' }, ], }, { key: 'mp4', title: 'MP4', subtitle: 'Pobierz wideo', icon: '■', accent: '#3b82f6', button: 'POBIERZ WIDEO', formats: [ { value: 'best', label: 'MP4 najlepsza' }, { value: '2160', label: 'MP4 2160p (4K)' }, { value: '1440', label: 'MP4 1440p' }, { value: '1080', label: 'MP4 1080p' }, { value: '720', label: 'MP4 720p' }, { value: '480', label: 'MP4 480p' }, ], }, { key: 'playlist', title: 'PLAYLISTA', subtitle: 'Pobierz całą listę', icon: '▦', accent: '#22c55e', button: 'POBIERZ PLAYLISTĘ', formats: [ { value: 'video-1080', label: 'Wideo MP4 1080p' }, { value: 'video-720', label: 'Wideo MP4 720p' }, { value: 'audio-320', label: 'Audio MP3 320 kbps' }, ], }, { key: 'facebook', title: 'FACEBOOK', subtitle: 'Pobierz wideo', icon: 'f', accent: '#60a5fa', button: 'POBIERZ WIDEO', formats: [ { value: 'best', label: 'MP4 najlepsza' }, { value: '1080', label: 'MP4 1080p' }, { value: '720', label: 'MP4 720p' }, { value: '480', label: 'MP4 480p' }, ], }, ]; const VERSION = '6.5.6'; export default function App() { const [url, setUrl] = useState(''); const [mode, setMode] = useState('mp3'); const [format, setFormat] = useState('320'); const [subtitles, setSubtitles] = useState(false); const [subsMode, setSubsMode] = useState<'embed' | 'file'>('embed'); const [busy, setBusy] = useState(false); const [progress, setProgress] = useState(0); const [log, setLog] = useState(['OK: wszystko gotowe do pobierania.']); const [ytdlp, setYtdlp] = useState<{ version: string; autoUpdate: boolean; updating?: boolean } | null>(null); const esRef = useRef(null); const jobRef = useRef(null); const logEndRef = useRef(null); const def = MODES.find((m) => m.key === mode)!; useEffect(() => { // przy zmianie trybu ustaw domyślny format setFormat(MODES.find((m) => m.key === mode)!.formats[0].value); }, [mode]); useEffect(() => { logEndRef.current?.scrollIntoView({ block: 'end' }); }, [log]); useEffect(() => { fetch('/api/ytdlp') .then((r) => r.json()) .then(setYtdlp) .catch(() => {}); }, []); async function checkUpdate() { setYtdlp((y) => (y ? { ...y, updating: true } : y)); appendLog('Sprawdzam aktualizacje yt-dlp...'); try { const r = await fetch('/api/ytdlp/update', { method: 'POST' }); const res = await r.json(); appendLog( res.updated ? `yt-dlp zaktualizowany do ${res.version}.` : `yt-dlp jest aktualny (${res.version}).`, ); setYtdlp((y) => (y ? { ...y, version: res.version, updating: false } : y)); } catch { appendLog('Nie udało się sprawdzić aktualizacji yt-dlp.'); setYtdlp((y) => (y ? { ...y, updating: false } : y)); } } function appendLog(line: string) { setLog((l) => [...l.slice(-400), line]); } async function paste() { try { const t = await navigator.clipboard.readText(); if (t) setUrl(t.trim()); } catch { appendLog('Nie udało się odczytać schowka (brak uprawnień przeglądarki).'); } } function reset() { esRef.current?.close(); esRef.current = null; jobRef.current = null; setBusy(false); } async function start() { if (!url.trim()) { appendLog('BŁĄD: wklej najpierw link.'); return; } setBusy(true); setProgress(0); setLog([`Start: ${def.title} — ${url.trim()}`]); try { const res = await fetch('/api/jobs', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url.trim(), mode, format, subtitles, subsMode }), }); if (!res.ok) throw new Error(`HTTP ${res.status}`); const { id } = await res.json(); jobRef.current = id; const es = new EventSource(`/api/jobs/${id}/events`); esRef.current = es; es.onmessage = (e) => handleEvent(e.data); es.onerror = () => { // strumień zamknięty (koniec joba lub błąd sieci) es.close(); }; } catch (e: any) { appendLog(`BŁĄD: ${e?.message || e}`); reset(); } } function handleEvent(data: string) { let ev: any; try { ev = JSON.parse(data); } catch { return; } switch (ev.kind) { case 'log': appendLog(ev.line); break; case 'progress': setProgress(ev.percent); break; case 'done': { setProgress(100); appendLog(`Pobieranie zakończone: ${ev.file?.name}`); // wywołaj pobranie pliku przez przeglądarkę const a = document.createElement('a'); a.href = ev.downloadUrl; a.download = ev.file?.name || ''; document.body.appendChild(a); a.click(); a.remove(); reset(); break; } case 'error': appendLog(`BŁĄD: ${ev.message}`); reset(); break; default: break; } } async function stop() { const id = jobRef.current; if (id) { try { await fetch(`/api/jobs/${id}`, { method: 'DELETE' }); } catch { /* ignore */ } } appendLog('Zatrzymano.'); reset(); } return (

Video & Sound Downloader Pro

Pobieraj audio, wideo i playlisty z jednego panelu

v{VERSION}
setUrl(e.target.value)} />
{MODES.map((m) => ( ))}

Napisy dotyczą tylko faktycznie dostępnych napisów — nie obejmują automatycznie generowanych napisów YouTube. Gotowy plik pobiera się przez przeglądarkę (folder „Pobrane”).

{busy && (
)}
Log
{log.map((l, i) => (
{l}
))}
); }