import { useState, useRef, useEffect } from 'react'; import { apiDownloaderDownloadRetrieve, } from '@/api/generated/public/downloader'; import { type VideoInfoResponse } from '@/api/generated/public/models'; import { FaLink, FaVideo, FaVolumeUp, FaFile, FaFont, FaCookie } from 'react-icons/fa'; import Ad from './Ad'; import Statistics from './Statistics'; import { publicApi } from '@/api/publicClient'; // Common file extensions supported by ffmpeg const FILE_EXTENSIONS = [ { value: 'mp4', label: 'MP4 (H.264 + AAC, most compatible)' }, { value: 'mkv', label: 'MKV (Flexible, lossless container)' }, { value: 'webm', label: 'WebM (VP9/AV1 + Opus)' }, { value: 'avi', label: 'AVI (Older format)' }, { value: 'mov', label: 'MOV (Apple-friendly)' }, { value: 'flv', label: 'FLV (Legacy)' }, { value: 'ogg', label: 'OGG (Audio/Video)' }, ]; export default function Downloader() { const [videoUrl, setVideoUrl] = useState(''); const [videoInfo, setVideoInfo] = useState(null); const [isLoading, setIsLoading] = useState(false); const [isDownloading, setIsDownloading] = useState(false); const [downloadStatus, setDownloadStatus] = useState(''); const [downloadProgress, setDownloadProgress] = useState(null); const [error, setError] = useState(null); const wsRef = useRef(null); // Basic download options const [selectedVideoQuality, setSelectedVideoQuality] = useState(''); const [selectedAudioQuality, setSelectedAudioQuality] = useState(''); const [selectedExtension, setSelectedExtension] = useState('mp4'); // Playlist selection const [selectedVideos, setSelectedVideos] = useState([]); // Advanced options const [subtitles, setSubtitles] = useState(''); const [embedSubtitles, setEmbedSubtitles] = useState(false); const [embedThumbnail, setEmbedThumbnail] = useState(false); const [cookies, setCookies] = useState(''); const [showAdvanced, setShowAdvanced] = useState(false); // Helper functions for playlist selection const toggleVideoSelection = (videoIndex: number) => { setSelectedVideos(prev => prev.includes(videoIndex) ? prev.filter(i => i !== videoIndex) : [...prev, videoIndex] ); }; const selectAllVideos = () => { if (videoInfo?.videos) { setSelectedVideos(videoInfo.videos.map((_, index) => index + 1)); } }; const deselectAllVideos = () => { setSelectedVideos([]); }; // Cleanup WebSocket on unmount useEffect(() => { return () => { if (wsRef.current) { wsRef.current.close(); } }; }, []); const getAllVideoQualities = () => { if (!videoInfo?.videos) return []; const allQualities = new Set(); videoInfo.videos.forEach(video => { video.video_resolutions.forEach(quality => allQualities.add(quality)); }); return Array.from(allQualities).sort((a, b) => { const aNum = parseInt(a.replace('p', '')); const bNum = parseInt(b.replace('p', '')); return bNum - aNum; }); }; const getAllAudioQualities = () => { if (!videoInfo?.videos) return []; const allQualities = new Set(); videoInfo.videos.forEach(video => { video.audio_resolutions.forEach(quality => allQualities.add(quality)); }); return Array.from(allQualities).sort((a, b) => { const aNum = parseInt(a.replace('kbps', '')); const bNum = parseInt(b.replace('kbps', '')); return bNum - aNum; }); }; async function retrieveVideoInfo() { setIsLoading(true); setError(null); setVideoInfo(null); setSelectedVideos([]); try { const info = await apiDownloaderDownloadRetrieve({ url: videoUrl }); setVideoInfo(info); // If it's a playlist, select all videos by default if (info.is_playlist && info.videos) { setSelectedVideos(info.videos.map((_, index) => index + 1)); } } catch (err: any) { const errorMessage = err.response?.data?.error || err.message; setError({ error: errorMessage }); console.error('Retrieve video info error:', err); } finally { setIsLoading(false); } } async function handleDownload() { if (!videoUrl) { setError({ error: 'Please enter a URL first' }); return; } setIsDownloading(true); setDownloadStatus('Connecting…'); setDownloadProgress(null); setError(null); try { const videoQuality = selectedVideoQuality ? parseInt(selectedVideoQuality.replace('p', '')) : undefined; const audioQuality = selectedAudioQuality ? parseInt(selectedAudioQuality.replace('kbps', '')) : undefined; // Connect to WebSocket const wsUrl = `ws://localhost:8000/ws/downloader/`; const ws = new WebSocket(wsUrl); wsRef.current = ws; await new Promise((resolve, reject) => { ws.onopen = () => { setDownloadStatus('Starting…'); // Send download parameters ws.send(JSON.stringify({ url: videoUrl, ext: selectedExtension, video_quality: videoQuality, audio_quality: audioQuality, selected_videos: videoInfo?.is_playlist && selectedVideos.length > 0 ? selectedVideos : null, subtitles: subtitles || null, embed_subtitles: embedSubtitles, embed_thumbnail: embedThumbnail, cookies: cookies || null, })); }; ws.onmessage = (event) => { const data = JSON.parse(event.data); if (data.type === 'status') { setDownloadStatus(data.message); } else if (data.type === 'progress') { setDownloadStatus(data.message); setDownloadProgress(data.percent); } else if (data.type === 'done') { setDownloadStatus('Finalizing download...'); setDownloadProgress(100); // Přidáme responseType: 'blob', aby Axios/Fetch vrátil soubor správně publicApi.post('/api/downloader/download/file/', { token: data.token }, { responseType: 'blob' } ) .then((response) => { // 1. Vytvoření dočasné URL z přijatého Blobu const url = window.URL.createObjectURL(new Blob([response.data])); // 2. Extrakce názvu souboru z hlaviček (pokud ho backend posílá) let filename = `${data.filename || 'video'}.${selectedExtension}`; // 3. Vytvoření neviditelného odkazu pro spuštění stahování const link = document.createElement('a'); link.href = url; link.setAttribute('download', filename); // Zajišťuje stáhnutí místo otevření document.body.appendChild(link); // 4. Simulace kliknutí link.click(); // 5. Úklid po stažení link.remove(); window.URL.revokeObjectURL(url); setDownloadStatus('Download complete!'); resolve(); }) .catch((err) => { console.error('Failed to download blob:', err); reject(new Error('Failed to retrieve the file after processing.')); }); } else if (data.type === 'error') { reject(new Error(data.message)); } }; ws.onerror = () => { reject(new Error('WebSocket connection failed')); }; ws.onclose = () => { if (wsRef.current === ws) { wsRef.current = null; } }; }); } catch (err: any) { const errorMessage = err.message || 'Failed to download video'; setError({ error: errorMessage }); } finally { if (wsRef.current) { wsRef.current.close(); wsRef.current = null; } setIsDownloading(false); setDownloadStatus(''); setDownloadProgress(null); } } return (

Video Downloader

Video URL
setVideoUrl(e.target.value)} placeholder="Paste video URL here (YouTube, TikTok, Vimeo, etc.)" className="w-full p-3 border rounded" />
{error && (
Error: {error.error}
)} {videoInfo && (
{videoInfo.is_playlist ? (

📋 {videoInfo.playlist_title || 'Playlist'}

{videoInfo.playlist_count} videos found

{/* Playlist Video Selection */}

Select Videos to Download:

{videoInfo.videos.map((video, index) => { const videoNumber = index + 1; return (
); })}
{selectedVideos.length} of {videoInfo.videos.length} videos selected
) : (

🎥 {videoInfo.videos[0]?.title || 'Video'}

{videoInfo.videos[0]?.thumbnail && ( {videoInfo.videos[0].title} )} {videoInfo.videos[0]?.duration && (

Duration: {Math.floor(videoInfo.videos[0].duration / 60)}:{String(videoInfo.videos[0].duration % 60).padStart(2, '0')}

)}
)} {/* Quality and Format Selection */}
{/* Video Quality Dropdown */}
{/* Audio Quality Dropdown */}
{/* File Extension Dropdown */}
{isDownloading && downloadStatus && (
{downloadStatus}
{downloadProgress !== null && (
)}
)} {videoInfo.is_playlist && selectedVideos.length === 0 && (

Please select at least one video to download

)} {/* Advanced Options Toggle */}
{showAdvanced && (
{/* Subtitles */}
setSubtitles(e.target.value)} placeholder="e.g., 'en', 'en,cs', or 'all'" className="w-full p-2 border rounded" />

Language codes (e.g., 'en', 'cs') or 'all' for all available

{/* Checkboxes Row */}
{/* Cookies for Age-Restricted Content */}