import React, { useState, useEffect, useCallback } from 'react'; import { MediaList } from '../components/MediaList'; import { MediaItem } from '../api/jellyfin'; import { Loader } from '../components/Loader'; import { usePageTitle } from '../context/PageTitleContext/PageTitleContext'; import { useNavigate } from 'react-router-dom'; import { PullToRefreshWrapper } from '../components/PullToRefreshWrapper'; import { useSocket } from '../hooks/useSocket'; import { useDownloadContext } from '../context/DownloadContext/DownloadContext'; import { toast } from 'sonner'; import './Discover.css'; interface DiscoverySection { title: string; description?: string; tracks: MediaItem[]; } interface AlbumDiscoverySection { title: string; description?: string; albums: MediaItem[]; } interface GenreSection { genre: string; title: string; tracks: MediaItem[]; } interface DiscoveryData { for_you?: DiscoverySection; genres?: GenreSection[]; new_releases?: DiscoverySection; trending?: DiscoverySection; trending_albums?: AlbumDiscoverySection; new_albums?: AlbumDiscoverySection; } export const Discover: React.FC = () => { const { setPageTitle } = usePageTitle(); const [discoveryData, setDiscoveryData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [downloadStatus, setDownloadStatus] = useState<{ [key: string]: any }>({}); const navigate = useNavigate(); // Socket.io and download context integration const { socket, isConnected } = useSocket(); const downloadContext = useDownloadContext(); const { downloads: contextDownloads } = downloadContext || { downloads: {} }; const fetchDiscoveryData = useCallback(async () => { setLoading(true); setError(null); try { // Determine the correct backend URL based on environment let fetchUrl; // Check if we're on the live site if (window.location.hostname === 'music.samcloud.ca') { // Use relative URL for live site (will use the same domain with reverse proxy) fetchUrl = '/api/discover'; } else { // Use development backend URL for localhost const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; fetchUrl = `${backendUrl}/api/discover`; } console.log('Environment:', window.location.hostname); console.log('Fetching comprehensive discovery data from:', fetchUrl); const response = await fetch(fetchUrl); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } const data = await response.json(); console.log('Discovery data received:', data); setDiscoveryData(data); } catch (e: any) { console.error("Failed to fetch discovery data:", e); setError(e.message || 'Failed to fetch discovery content.'); } finally { setLoading(false); } }, []); // Socket.io event handlers for download status useEffect(() => { if (!socket) return; const handleDownloadUpdate = (data: any) => { if (data.history && Array.isArray(data.history)) { const newStatus: { [key: string]: any } = {}; data.history.forEach((item: any) => { if (item.id) { newStatus[item.id] = { status: item.status, progress: item.progress || 0, name: item.name }; // Update MediaItem offlineState in discovery data setDiscoveryData(prev => { const updated = { ...prev }; // Update offlineState in all sections const updateItemsOfflineState = (items: MediaItem[]) => { return items.map(mediaItem => { const spotifyData = (mediaItem as any).spotifyData || (mediaItem as any).searchResult; if (spotifyData && spotifyData.id === item.id) { // Map server status to offlineState let offlineState: string | undefined; switch (item.status) { case 'Queued...': case 'Preparing...': offlineState = 'accepted'; break; case 'Downloading...': offlineState = 'downloading'; break; case 'Processing...': offlineState = 'processing'; break; case 'Complete': offlineState = 'finished'; break; case 'Failed': case 'Error': case 'Cancelled': offlineState = undefined; break; default: offlineState = 'downloading'; } return { ...mediaItem, offlineState }; } return mediaItem; }); }; if (updated.for_you?.tracks) { updated.for_you.tracks = updateItemsOfflineState(updated.for_you.tracks); } if (updated.new_releases?.tracks) { updated.new_releases.tracks = updateItemsOfflineState(updated.new_releases.tracks); } if (updated.trending?.tracks) { updated.trending.tracks = updateItemsOfflineState(updated.trending.tracks); } if (updated.trending_albums?.albums) { updated.trending_albums.albums = updateItemsOfflineState(updated.trending_albums.albums); } if (updated.new_albums?.albums) { updated.new_albums.albums = updateItemsOfflineState(updated.new_albums.albums); } if (updated.genres) { updated.genres = updated.genres.map(genre => ({ ...genre, tracks: updateItemsOfflineState(genre.tracks) })); } return updated; }); } }); setDownloadStatus(prev => ({ ...prev, ...newStatus })); } }; socket.on('update_status', handleDownloadUpdate); return () => { socket.off('update_status', handleDownloadUpdate); }; }, [socket]); // Custom download handler for Discover tracks const handleDiscoverDownload = useCallback((item: MediaItem) => { if (!socket) { toast.error('Not connected to the server.'); return; } // Check if it's a Spotify-based track with metadata const spotifyData = (item as any).spotifyData || (item as any).searchResult; if (spotifyData && spotifyData.id) { console.log('🔽 Starting Spotify download for Discover track:', spotifyData); toast.info(`Downloading ${spotifyData.name}...`); // Immediately update the item's offlineState for UI feedback item.offlineState = 'downloading'; // Use the Spotify download endpoint socket.emit('download', { url: spotifyData.url, searchResult: spotifyData, name: spotifyData.name, artist: spotifyData.artist, type: spotifyData.type, id: spotifyData.id }); // Update local download status for immediate UI feedback const downloadKey = spotifyData.id; setDownloadStatus(prevStatus => ({ ...prevStatus, [downloadKey]: { status: 'Downloading...', progress: 0, id: spotifyData.id, name: spotifyData.name } })); // Also update the download context for UI feedback if (downloadContext && downloadContext.updateDownloadStatus) { downloadContext.updateDownloadStatus(downloadKey, 'downloading', 0); } } else { // Fallback to regular download context for Jellyfin tracks console.log('🔽 Using standard download for Jellyfin track:', item); if (downloadContext) { downloadContext.addToDownloads([item], undefined); } } }, [socket, downloadContext]); useEffect(() => { setPageTitle('Discover'); fetchDiscoveryData(); }, [setPageTitle, fetchDiscoveryData]); if (loading) { return (

Discover

Finding new music just for you...

); } if (error) { return (

Discover

Explore new music tailored to your taste

{error}

We're having trouble loading new music recommendations. Please try again later.

); } const hasContent = Object.keys(discoveryData).length > 0; if (!hasContent) { return (

Discover

Explore new music tailored to your taste

No recommendations available

Start listening to music to get personalized recommendations!

); } return (

Discover

Explore new music tailored to your taste

{/* For You Section */} {discoveryData.for_you && discoveryData.for_you.tracks.length > 0 && (

{discoveryData.for_you.title}

{discoveryData.for_you.description}

)} {/* New Releases Section */} {discoveryData.new_releases && discoveryData.new_releases.tracks.length > 0 && (

{discoveryData.new_releases.title}

{discoveryData.new_releases.description}

)} {/* Trending Section */} {discoveryData.trending && discoveryData.trending.tracks.length > 0 && (

{discoveryData.trending.title}

{discoveryData.trending.description}

)} {/* Popular Albums Section */} {discoveryData.trending_albums && discoveryData.trending_albums.albums.length > 0 && (

{discoveryData.trending_albums.title}

{discoveryData.trending_albums.description}

)} {/* New Albums Release Section */} {discoveryData.new_albums && discoveryData.new_albums.albums.length > 0 && (

New Albums Release

Fresh albums just for you

)} {/* Genre Sections */} {discoveryData.genres && discoveryData.genres.length > 0 && ( <>

Explore by Genre

Discover new music across different genres

{discoveryData.genres.map((genreSection: GenreSection, index: number) => (

{genreSection.title}

))} )}
); };