'use client'; import { useState, useRef, useEffect, useCallback } from 'react'; import { PiShareFat } from 'react-icons/pi'; import { TfiDownload } from 'react-icons/tfi'; interface VideoFormat { format_id: string; format_note: string; ext: string; resolution: string; filesize: number; type: string; has_audio?: boolean; url?: string; } declare global { interface Window { FFmpeg: any; FFmpegWASM: any; } } function getQualityLabel(resolution: string): string { const height = parseInt(resolution) || 0; if (height >= 3840) return '4K UHD'; if (height >= 2560) return '2K QHD'; if (height >= 1920) return 'Full HD 1080p'; if (height >= 1280) return 'HD 720p'; if (height >= 854) return 'SD 480p'; if (height >= 640) return 'SD 360p'; if (height >= 426) return 'SD 240p'; if (height >= 256) return 'SD 144p'; return resolution || 'Unknown'; } function getQualityBadge(height: number): { label: string; color: string } | null { if (height >= 3840) return { label: '4K', color: '#ff0000' }; if (height >= 2560) return { label: '2K', color: '#ff6b00' }; if (height >= 1920) return { label: 'HD', color: '#00a0ff' }; if (height >= 1280) return { label: 'HD', color: '#00a0ff' }; return null; } export default function WatchActions({ videoId }: { videoId: string }) { const [isDownloading, setIsDownloading] = useState(false); const [showFormats, setShowFormats] = useState(false); const [formats, setFormats] = useState([]); const [audioFormats, setAudioFormats] = useState([]); const [isLoadingFormats, setIsLoadingFormats] = useState(false); const [downloadProgress, setDownloadProgress] = useState(''); const [progressPercent, setProgressPercent] = useState(0); const [ffmpegLoaded, setFfmpegLoaded] = useState(false); const [ffmpegLoading, setFfmpegLoading] = useState(false); const menuRef = useRef(null); const ffmpegRef = useRef(null); const loadFFmpeg = useCallback(async () => { if (ffmpegLoaded || ffmpegLoading) return; setFfmpegLoading(true); setDownloadProgress('Loading video processor...'); try { const script = document.createElement('script'); script.src = 'https://unpkg.com/@ffmpeg/ffmpeg@0.12.7/dist/umd/ffmpeg.js'; script.async = true; document.head.appendChild(script); const coreScript = document.createElement('script'); coreScript.src = 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js'; coreScript.async = true; document.head.appendChild(coreScript); await new Promise((resolve) => { const checkLoaded = () => { if (window.FFmpeg && window.FFmpeg.FFmpeg) { resolve(); } else { setTimeout(checkLoaded, 100); } }; checkLoaded(); }); const { FFmpeg } = window.FFmpeg; const ffmpeg = new FFmpeg(); await ffmpeg.load({ coreURL: 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.js', wasmURL: 'https://unpkg.com/@ffmpeg/core@0.12.6/dist/umd/ffmpeg-core.wasm', }); ffmpegRef.current = ffmpeg; setFfmpegLoaded(true); } catch (e) { console.error('Failed to load FFmpeg:', e); } finally { setFfmpegLoading(false); } }, [ffmpegLoaded, ffmpegLoading]); useEffect(() => { const handleClickOutside = (event: MouseEvent) => { if (menuRef.current && !menuRef.current.contains(event.target as Node)) { setShowFormats(false); } }; document.addEventListener('mousedown', handleClickOutside); return () => document.removeEventListener('mousedown', handleClickOutside); }, []); const handleShare = () => { if (typeof window !== 'undefined') { const url = window.location.href; navigator.clipboard.writeText(url).then(() => { alert('Link copied to clipboard!'); }).catch(() => { alert('Failed to copy link'); }); } }; const fetchFormats = async () => { if (showFormats) { setShowFormats(false); return; } setShowFormats(true); if (formats.length > 0) return; setIsLoadingFormats(true); try { const res = await fetch(`/api/formats?v=${encodeURIComponent(videoId)}`); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); if (Array.isArray(data)) { const videoFormats = data.filter((f: VideoFormat) => (f.type === 'video' || f.type === 'both') && !f.format_note?.toLowerCase().includes('storyboard') && f.ext === 'mp4' ).sort((a: VideoFormat, b: VideoFormat) => { const resA = parseInt(a.resolution) || 0; const resB = parseInt(b.resolution) || 0; return resB - resA; }); const audioOnly = data.filter((f: VideoFormat) => f.type === 'audio' || (f.resolution === 'audio only') ).sort((a: VideoFormat, b: VideoFormat) => (b.filesize || 0) - (a.filesize || 0)); setFormats(videoFormats.length > 0 ? videoFormats : data.filter((f: VideoFormat) => f.ext === 'mp4').slice(0, 10)); setAudioFormats(audioOnly); } } catch (e) { console.error('Failed to fetch formats:', e); } finally { setIsLoadingFormats(false); } }; const fetchFile = async (url: string, label: string): Promise => { setDownloadProgress(`Downloading ${label}...`); const tryDirect = async (): Promise => { try { const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 5000); const response = await fetch(url, { signal: controller.signal, mode: 'cors' }); clearTimeout(timeoutId); return response.ok ? response : null; } catch { return null; } }; const tryProxy = async (): Promise => { return fetch(`/api/proxy-file?url=${encodeURIComponent(url)}`); }; let response = await tryDirect(); if (!response) { setDownloadProgress(`Connecting via proxy...`); response = await tryProxy(); } if (!response.ok) throw new Error(`Failed to fetch ${label}`); const contentLength = response.headers.get('content-length'); const total = contentLength ? parseInt(contentLength, 10) : 0; let loaded = 0; const reader = response.body?.getReader(); if (!reader) throw new Error('No reader available'); const chunks: Uint8Array[] = []; while (true) { const { done, value } = await reader.read(); if (done) break; chunks.push(value); loaded += value.length; if (total > 0) { const percent = Math.round((loaded / total) * 100); setProgressPercent(percent); setDownloadProgress(`${label}: ${percent}%`); } } const result = new Uint8Array(loaded); let offset = 0; for (const chunk of chunks) { result.set(chunk, offset); offset += chunk.length; } return result; }; const downloadBlob = (blob: Blob, filename: string) => { const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); }; const handleDownload = async (format?: VideoFormat) => { setIsDownloading(true); setShowFormats(false); setProgressPercent(0); setDownloadProgress('Preparing download...'); try { // Simple approach: use the backend's download-file endpoint const downloadUrl = `/api/download-file?v=${encodeURIComponent(videoId)}${format ? `&f=${encodeURIComponent(format.format_id)}` : ''}`; // Create a temporary anchor tag to trigger download const a = document.createElement('a'); a.href = downloadUrl; a.download = ''; // Let the server set filename via Content-Disposition document.body.appendChild(a); a.click(); document.body.removeChild(a); setDownloadProgress('Download started!'); setProgressPercent(100); // Reset after a short delay setTimeout(() => { setIsDownloading(false); setDownloadProgress(''); setProgressPercent(0); }, 2000); } catch (e: any) { console.error(e); alert(e.message || 'Download failed. Please try again.'); setIsDownloading(false); setDownloadProgress(''); setProgressPercent(0); } }; const formatFileSize = (bytes: number): string => { if (!bytes || bytes <= 0) return ''; if (bytes >= 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`; if (bytes >= 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`; return `${(bytes / 1024).toFixed(0)} KB`; }; return (
{showFormats && (
setShowFormats(false)} /> )} {showFormats && (
Select Quality
{isLoadingFormats ? (
Loading...
) : formats.length === 0 ? (
No formats available
) : ( formats.map(f => { const height = parseInt(f.resolution) || 0; const badge = getQualityBadge(height); return ( ); }) )}
)}
{isDownloading && downloadProgress && ( {downloadProgress} )}
); }