import { useState, useEffect, useRef, useMemo } from 'react'; import { useSocket } from '../hooks/useSocket'; import { useDebounce } from '../hooks/useDebounce'; import { SearchIcon } from '../components/SvgIcons'; import { toast } from 'sonner'; import { useDownloadContext } from '../context/DownloadContext/DownloadContext'; import { MediaItem } from '../api/jellyfin'; import { MediaList } from '../components/MediaList'; import './OnlineSearch.css'; // Modal component for displaying detailed information const DetailModal = ({ item, onClose }: { item: SearchResult | null; onClose: () => void }) => { if (!item) return null; return (
e.stopPropagation()}>
{item.name}

{item.name}

{item.artist}

{item.album &&

Album: {item.album}

} {item.type.toUpperCase()}

Details

{item.duration && (

Duration: {Math.floor(item.duration / 60000)}:{String(Math.floor((item.duration % 60000) / 1000)).padStart(2, '0')}

)} {item.total_tracks && (

Total Tracks: {item.total_tracks}

)} {item.release_date && (

Release Date: {item.release_date}

)}
🎵 Open in Spotify
); }; interface SearchResult { id: string; name: string; artist: string; album: string; image: string; type: 'track' | 'album' | 'playlist' | 'artist'; duration?: number; url: string; total_tracks?: number; release_date?: string; } interface DownloadStatus { status: string; progress?: number; id?: string; name?: string; } export const OnlineSearch = () => { const [query, setQuery] = useState(''); const [searchType, setSearchType] = useState<'track' | 'album' | 'playlist' | 'artist'>('track'); const [results, setResults] = useState([]); const [loading, setLoading] = useState(false); const [downloadStatus, setDownloadStatus] = useState<{ [key: string]: DownloadStatus }>({}); const [selectedItem, setSelectedItem] = useState(null); const { socket, isConnected } = useSocket(); const downloadContext = useDownloadContext(); const { downloads: contextDownloads } = downloadContext || { downloads: {} }; const containerRef = useRef(null); const searchTimeoutRef = useRef(null); const handleSearch = () => { if (!query.trim()) { toast.error('Please enter a search query.'); return; } if (!socket) { toast.error('Not connected to the server.'); return; } if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); } setLoading(true); socket.emit('search', { query, type: searchType }); searchTimeoutRef.current = setTimeout(() => { setLoading(false); toast.error('Search timed out. Please try again.'); searchTimeoutRef.current = null; }, 10000); }; useEffect(() => { if (!socket) return; const handleSearchResults = (data: { results: SearchResult[] }) => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); searchTimeoutRef.current = null; } setResults(data.results || []); setLoading(false); }; const handleSearchError = (error: any) => { if (searchTimeoutRef.current) { clearTimeout(searchTimeoutRef.current); searchTimeoutRef.current = null; } toast.error(error.message || 'An error occurred during search.'); setLoading(false); }; const handleDownloadUpdate = (data: any) => { console.log('Download update received:', data); if (data.id && data.status) { setDownloadStatus(prev => ({ ...prev, [data.id]: { status: data.status, progress: data.progress || 0, id: data.id, name: data.name } })); // Update the MediaItem offlineState in DownloadContext if (downloadContext && downloadContext.updateDownloadStatus) { // Map the status to the offlineState values let offlineState: string | undefined = 'downloading'; switch (data.status) { case 'Downloading': case 'downloading': offlineState = 'downloading'; break; case 'Downloading...': offlineState = 'processing'; break; case 'Complete': offlineState = 'finished'; break; case 'downloaded': offlineState = 'downloaded'; break; case 'Failed': offlineState = undefined; break; case 'Accepted': case 'accepted': offlineState = 'accepted'; break; case 'Uploading': case 'uploading': offlineState = 'uploading'; break; default: offlineState = data.status; } // Call updateDownloadStatus with the download ID and status downloadContext.updateDownloadStatus(data.id, offlineState, data.progress || 0); } // Show live progress updates in toast for better feedback if (data.status === 'Downloading...' && data.progress) { // Only show progress updates every 10% to avoid spam const progress = Math.floor(data.progress); if (progress % 10 === 0 || progress === 100) { toast.info(`${data.name || 'Item'}: ${progress}% downloaded`, { duration: 2000, position: 'bottom-center' // More visible position }); } // Always update the toast with latest progress for smooth experience if (progress > 0 && progress < 100) { toast.info(`${data.name || 'Item'}: ${progress}% downloaded`, { id: `download-${data.id}`, // Use consistent ID to update same toast duration: 2000, position: 'bottom-center' }); } } else if (data.status === 'Complete') { toast.success(`${data.name || 'Item'} downloaded successfully!`, { position: 'bottom-center' }); } else if (data.status === 'Failed') { toast.error(`${data.name || 'Item'} failed to download.`, { position: 'bottom-center' }); } } }; socket.on('search_results', handleSearchResults); socket.on('search_error', handleSearchError); socket.on('update_status', handleDownloadUpdate); return () => { socket.off('search_results', handleSearchResults); socket.off('search_error', handleSearchError); socket.off('update_status', handleDownloadUpdate); }; }, [socket]); const debouncedSearch = useDebounce(handleSearch, 500); useEffect(() => { if (query.trim() !== '') { debouncedSearch(); } else { setResults([]); setLoading(false); } }, [query, searchType]); // Remove debouncedSearch to prevent infinite loops // Custom download handler for Spotify tracks const handleSpotifyDownload = (item: MediaItem) => { if (!socket) { toast.error('Not connected to the server.'); return; } const originalResult = (item as any).searchResult; if (!originalResult) { toast.error('Invalid search result.'); return; } console.log('🔽 Starting Spotify download for:', originalResult); toast.info(`Downloading ${originalResult.name}...`); socket.emit('download', { url: originalResult.url, searchResult: originalResult, name: originalResult.name, artist: originalResult.artist, type: originalResult.type, id: originalResult.id }); // Update local download status for immediate UI feedback const downloadKey = originalResult.id || originalResult.url; setDownloadStatus(prevStatus => ({ ...prevStatus, [downloadKey]: { status: 'Downloading...', progress: 0, id: originalResult.id, name: originalResult.name } })); // Also update the MediaItem's offlineState immediately for UI feedback if (downloadContext && downloadContext.updateDownloadStatus) { downloadContext.updateDownloadStatus(downloadKey, 'downloading', 0); } }; // Convert search results to MediaItem format for MediaList component const convertToMediaItems = (searchResults: SearchResult[]): MediaItem[] => { return searchResults.map(result => { const downloadKey = result.id || result.url; const currentDownloadStatus = downloadStatus[downloadKey]; const contextDownloadStatus = contextDownloads[downloadKey]; // Use context download status if available, otherwise use local status const activeStatus = contextDownloadStatus || currentDownloadStatus; // Map download status to offlineState let offlineState: MediaItem['offlineState'] = undefined; if (activeStatus) { const status = activeStatus.status || activeStatus; switch (status) { case 'Downloading...': case 'Downloading': case 'downloading': offlineState = 'downloading'; break; case 'Complete': offlineState = 'finished'; break; case 'downloaded': offlineState = 'downloaded'; break; case 'Failed': offlineState = undefined; break; case 'Accepted': case 'accepted': offlineState = 'accepted'; break; case 'Uploading': case 'uploading': offlineState = 'uploading'; break; default: offlineState = undefined; } } return { Id: result.id || result.url, // Use URL as fallback ID Name: result.name, Type: 'Audio', // Use BaseItemKind.Audio ServerId: 'spotify-search', RunTimeTicks: result.duration ? result.duration * 10000 : 0, IsFolder: false, MediaType: 'Audio', // Use both AlbumArtist and Artists for compatibility AlbumArtist: result.artist || 'Unknown Artist', Artists: result.artist ? [result.artist] : ['Unknown Artist'], Album: result.album || 'Unknown Album', UserData: { Played: false, IsFavorite: false }, // Spotify-specific fields for Discover mode SpotifyId: result.id, SpotifyUrl: result.url, SpotifyImage: result.image, // Store original search result data for download searchResult: result, // Add download status from our local state offlineState, downloadProgress: activeStatus?.progress || 0 } as MediaItem; }); }; const mediaItems = useMemo(() => convertToMediaItems(results), [results, downloadStatus, contextDownloads]); return (
{/* Search Header Section */}
Search & Download
Find and add music from Spotify to your library
setQuery(e.target.value)} placeholder="Search for music..." className="search-input" onKeyPress={(e) => e.key === 'Enter' && handleSearch()} />
{(['track', 'album', 'artist', 'playlist'] as const).map((type) => ( ))}
{!isConnected && (
Not connected to the server. Please check your connection.
)}
{/* Search Results Section - Styled like Home page */} {query.trim() !== '' && (
{loading ? 'Searching...' : `Search Results for "${query}"`}
{loading ? 'Finding music...' : results.length > 0 ? `Found ${results.length} results` : 'No results found'}
{loading ? (
) : results.length > 0 ? ( ) : (

No results found for "{query}". Try a different search term.

)}
)} setSelectedItem(null)} />
); };