import React, { useState, useEffect, useRef } from 'react'; import { useNavigate } from 'react-router-dom'; import { MediaItem } from '../api/jellyfin'; import { useJellyfinContext } from '../context/JellyfinContext/JellyfinContext'; import { usePlaybackContext } from '../context/PlaybackContext/PlaybackContext'; import { InstantMixService } from '../services/InstantMixService'; import { useInstantMix } from '../hooks/useInstantMix'; import { useJellyfinTracksData } from '../hooks/Jellyfin/Infinite/useJellyfinTracksData'; import { MediaList } from '../components/MediaList'; import './HomeEnhanced.css'; interface DailyMix { id: string; name: string; description: string; image: string; tracks: MediaItem[]; lastRefresh: number; } interface ThemedMix { title: string; description: string; theme: string; tracks: MediaItem[]; } interface DiscoverPreview { title: string; description?: string; tracks: MediaItem[]; } export const HomeEnhanced: React.FC = () => { const navigate = useNavigate(); const api = useJellyfinContext(); const { playTrack, setCurrentPlaylistSimple } = usePlaybackContext(); const { playInstantMix } = useInstantMix(); const { items: allTracks, isLoading: tracksLoading } = useJellyfinTracksData(); const [backgroundImages, setBackgroundImages] = useState([]); const [currentImageIndex, setCurrentImageIndex] = useState(0); const [dailyMixes, setDailyMixes] = useState([]); const [themedMixes, setThemedMixes] = useState([]); const [recentlyPlayed, setRecentlyPlayed] = useState([]); const [recommendedTracks, setRecommendedTracks] = useState([]); const [discoverPreview, setDiscoverPreview] = useState(null); const [showMixQueue, setShowMixQueue] = useState(null); const [mixQueueTracks, setMixQueueTracks] = useState([]); const [playingQueue, setPlayingQueue] = useState(false); // Loading states for individual sections const [loadingStates, setLoadingStates] = useState({ background: true, mixes: true, themedMixes: true, recentlyPlayed: true, recommendations: true, discover: true }); const instantMixService = InstantMixService.getInstance(); const imageRotationRef = useRef(null); const touchTimerRef = useRef(null); useEffect(() => { // Show UI immediately and load content in background if (!tracksLoading && allTracks.length > 0) { initializeHomeDataAsync(); } // UI shows immediately regardless of loading state return () => { if (imageRotationRef.current) { clearInterval(imageRotationRef.current); } }; }, [tracksLoading, allTracks]); // Start rotation when images are loaded useEffect(() => { setupImageRotation(); }, [backgroundImages]); const initializeHomeDataAsync = async () => { // Start loading everything in parallel without blocking UI const loadingPromises = [ loadBackgroundImages(), loadDailyMixes(), loadThemedMixes(), loadRecentlyPlayed() ]; // Load discover with its own error handling (independent of other content) setTimeout(() => { loadDiscoverPreview(); // Load discover with a slight delay to not block other content }, 500); Promise.all(loadingPromises).then(() => { // Generate recommendations after we have recently played generateRecommendedTracks(); }).catch((error) => { console.error('Error initializing home data:', error); }); // Start image rotation immediately if we have images setupImageRotation(); }; const loadBackgroundImages = async () => { try { // Get tracks with album art const tracksWithArt = allTracks .filter(track => track.AlbumId && track.ImageTags?.Primary) .slice(0, 8); // Take top 8 for rotation const images = tracksWithArt.map(track => api.getImageUrl(track, 'Primary', { width: 2560, height: 1440 }) // Higher resolution for full screen ); if (images.length > 0) { setBackgroundImages(images); // Rotation will start via useEffect } } catch (error) { console.error('Error loading background images:', error); } finally { setLoadingStates(prev => ({ ...prev, background: false })); } }; const setupImageRotation = () => { if (imageRotationRef.current) { clearInterval(imageRotationRef.current); } if (backgroundImages.length > 1) { imageRotationRef.current = setInterval(() => { setCurrentImageIndex(prev => (prev + 1) % backgroundImages.length); }, 6000); // Change every 6 seconds (faster rotation) } }; const loadDiscoverPreview = async () => { try { // Determine the correct backend URL based on environment let fetchUrl; if (window.location.hostname === 'music.samcloud.ca') { fetchUrl = '/api/discover'; } else { const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; fetchUrl = `${backendUrl}/api/discover`; } console.log('Home: Fetching discover data from:', fetchUrl); // Add timeout using AbortController const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 8000); // Increased to 8 seconds const response = await fetch(fetchUrl, { signal: controller.signal, headers: { 'Accept': 'application/json', } }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); console.log('Home: Received discover data:', data); // Try multiple sections for preview (prioritize for_you, then new_releases, then trending) if (data.for_you && data.for_you.tracks && data.for_you.tracks.length > 0) { console.log('Home: Using for_you section with', data.for_you.tracks.length, 'tracks'); setDiscoverPreview({ title: data.for_you.title || 'Made for You', description: data.for_you.description || 'Personalized tracks available for download', tracks: data.for_you.tracks.slice(0, 12) }); } else if (data.new_releases && data.new_releases.tracks && data.new_releases.tracks.length > 0) { console.log('Home: Using new_releases section with', data.new_releases.tracks.length, 'tracks'); setDiscoverPreview({ title: data.new_releases.title || 'New Releases', description: data.new_releases.description || 'Fresh tracks available for download', tracks: data.new_releases.tracks.slice(0, 12) }); } else if (data.trending && data.trending.tracks && data.trending.tracks.length > 0) { console.log('Home: Using trending section with', data.trending.tracks.length, 'tracks'); setDiscoverPreview({ title: data.trending.title || 'Trending Now', description: data.trending.description || 'Popular tracks available for download', tracks: data.trending.tracks.slice(0, 12) }); } else { console.log('Home: No valid backend sections found'); setDiscoverPreview({ title: 'Discover Unavailable', description: 'Backend discovery service returned no content', tracks: [] }); } } else { console.log('Home: Discover server responded with error:', response.status); setDiscoverPreview({ title: 'Discover Unavailable', description: 'Backend discovery service is not responding', tracks: [] }); } } catch (error) { console.log('Home: Discover server error:', error); // No local fallback - just show unavailable state setDiscoverPreview({ title: 'Discover Unavailable', description: 'Music discovery service is temporarily unavailable', tracks: [] }); } finally { setLoadingStates(prev => ({ ...prev, discover: false })); } }; const loadThemedMixes = async () => { try { // Get Spotify-based themed mixes from backend only let fetchUrl; if (window.location.hostname === 'music.samcloud.ca') { fetchUrl = '/api/discover/daily-mixes'; } else { const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; fetchUrl = `${backendUrl}/api/discover/daily-mixes`; } console.log('Home: Fetching Spotify themed mixes from:', fetchUrl); // Add timeout for backend const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), 10000); const response = await fetch(fetchUrl, { signal: controller.signal, headers: { 'Accept': 'application/json', } }); clearTimeout(timeoutId); if (response.ok) { const data = await response.json(); console.log('Home: Received Spotify themed mixes data:', data); if (data.mixes && Array.isArray(data.mixes) && data.mixes.length > 0) { // Rank mixes client-side against the user's library to surface playlists more likely to match the user's taste try { const ranked = rankThemedMixes(data.mixes); setThemedMixes(ranked); console.log(`Home: Set ${ranked.length} Spotify themed mixes from backend (ranked)`); } catch (err) { console.warn('Ranking themed mixes failed, falling back to raw mixes', err); setThemedMixes(data.mixes); } } else { console.log('Home: No Spotify themed mixes in response'); setThemedMixes([]); } } else { console.log('Home: Spotify themed mixes server error:', response.status); setThemedMixes([]); } } catch (error) { console.log('Home: Spotify themed mixes error:', error); setThemedMixes([]); } finally { setLoadingStates(prev => ({ ...prev, themedMixes: false })); } }; const loadDailyMixes = async () => { try { // Check for existing saved mixes const savedMixes = instantMixService.getSavedMixes(); // Check if mixes need refresh (refresh daily or if less than 3 mixes) const now = Date.now(); const oneDayMs = 24 * 60 * 60 * 1000; const needsRefresh = savedMixes.length < 6 || // Changed from 3 to 6 savedMixes.some(mix => (now - mix.createdAt) > oneDayMs); if (savedMixes.length > 0 && !needsRefresh) { const dailyMixData = savedMixes.map((mix: any) => ({ id: mix.id, name: mix.name, description: `${mix.tracks.length} songs • Based on ${mix.seedItem?.Name || 'your music'}`, image: getTrackImage(mix.tracks[0]), tracks: mix.tracks, lastRefresh: mix.createdAt })); setDailyMixes(dailyMixData); } else { // Generate new daily mixes await generateDailyMixes(); } } catch (error) { console.error('Error loading daily mixes:', error); } finally { setLoadingStates(prev => ({ ...prev, mixes: false })); } }; const generateDailyMixes = async () => { try { if (allTracks.length < 10) return; // Generate more varied types of mixes const mixTypes = [ { type: 'mood' as const, name: 'Chill Mix', description: 'Relaxing vibes' }, { type: 'mood' as const, name: 'Energy Mix', description: 'High energy tracks' }, { type: 'similar' as const, name: 'Discovery Mix', description: 'New discoveries' }, { type: 'artist' as const, name: 'Artist Radio', description: 'Based on your favorites' }, { type: 'genre' as const, name: 'Genre Mix', description: 'Your favorite genres' }, { type: 'mood' as const, name: 'Focus Mix', description: 'Perfect for concentration' } ]; const generatedMixes: DailyMix[] = []; for (const mixConfig of mixTypes) { // Use different seed tracks for variety const seedTrack = allTracks[Math.floor(Math.random() * allTracks.length)]; const mixTracks = await instantMixService.generateInstantMix({ seedItem: seedTrack, mixType: mixConfig.type, trackCount: 30 // Increased from 25 }, allTracks); if (mixTracks.length > 0) { const mixId = instantMixService.saveMixAsVirtualPlaylist( mixConfig.name, mixTracks, mixConfig.type, seedTrack ); generatedMixes.push({ id: mixId, name: mixConfig.name, description: `${mixTracks.length} songs • ${mixConfig.description}`, image: getTrackImage(mixTracks[0]), tracks: mixTracks, lastRefresh: Date.now() }); } } setDailyMixes(generatedMixes); } catch (error) { console.error('Error generating daily mixes:', error); } }; const loadRecentlyPlayed = async () => { try { const recent = await api.fetchRecentlyPlayed(0, 12); setRecentlyPlayed(recent); } catch (error) { console.error('Error loading recently played:', error); } finally { setLoadingStates(prev => ({ ...prev, recentlyPlayed: false })); } }; const generateRecommendedTracks = () => { try { // Simple recommendation: random tracks that aren't recently played const recentIds = new Set(recentlyPlayed.map(track => track.Id)); const availableTracks = allTracks.filter(track => !recentIds.has(track.Id)); // Shuffle and take first 8 const shuffled = [...availableTracks].sort(() => Math.random() - 0.5); setRecommendedTracks(shuffled.slice(0, 8)); } catch (error) { console.error('Error generating recommendations:', error); } finally { setLoadingStates(prev => ({ ...prev, recommendations: false })); } }; const handleMixTap = async (mix: DailyMix) => { // Simple single-click to play, like themed mixes setCurrentPlaylistSimple({ playlist: mix.tracks, title: mix.name }); playTrack(0); }; const handleMixDoubleClick = async (mix: DailyMix) => { // Double-click to show queue setMixQueueTracks(mix.tracks); setShowMixQueue(mix.id); }; const handleRecommendedTrackPlay = async (track: MediaItem) => { // Generate instant mix from this track try { await playInstantMix(track, 'similar'); } catch (error) { console.error('Error playing recommended track:', error); // Fallback to just playing the track setCurrentPlaylistSimple({ playlist: [track], title: track.Name || 'Unknown Track' }); playTrack(0); } }; const handleThemedMixPlay = async (themedMix: ThemedMix) => { try { console.log('Playing themed mix:', themedMix.title, 'with', themedMix.tracks.length, 'tracks'); if (themedMix.tracks.length === 0) { console.warn('No tracks in themed mix'); return; } // Set the playlist and play the first track setCurrentPlaylistSimple({ playlist: themedMix.tracks, title: themedMix.title }); playTrack(0); } catch (error) { console.error('Error playing themed mix:', error); } }; // Long-press / context handler to preview queue (touch friendly) const handleThemedMixLongPress = (mix: ThemedMix) => { if (!mix || !mix.tracks) return; setMixQueueTracks(mix.tracks); setShowMixQueue(mix.title || mix.theme || 'themed-queue'); }; // Attempt to extract a playlist id from backend mix object const getPlaylistId = (mix: any): string | null => { return mix.playlistId || mix.spotifyId || mix.id || mix.externalId || (mix.tracks && mix.tracks[0] && (mix.tracks[0].PlaylistId || mix.tracks[0].PlaylistId)) || null; }; const handlePlaylistDownload = async (mix: any) => { try { const id = getPlaylistId(mix); if (!id) { window.alert('Unable to determine playlist id for download.'); return; } const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; const downloadEndpoint = `${backendUrl}/api/spotify/download-playlist`; // POST to backend to initiate download of this Spotify playlist const resp = await fetch(downloadEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ playlistId: id }) }); if (resp.ok) { window.alert('Playlist download started. Check Downloads or server logs.'); } else { const text = await resp.text(); console.error('Playlist download failed:', text); window.alert('Playlist download failed. See console for details.'); } } catch (err) { console.error('Error initiating playlist download:', err); window.alert('Playlist download failed.'); } }; // Simple client-side ranking: prefer playlists containing your top artists const rankThemedMixes = (mixes: any[]) => { if (!allTracks || allTracks.length === 0) return mixes; const artistFreq = new Map(); for (const t of allTracks) { const artists = t.Artists || (t.AlbumArtist ? [t.AlbumArtist] : []); for (const a of artists) { if (!a) continue; const k = a.toLowerCase(); artistFreq.set(k, (artistFreq.get(k) || 0) + 1); } } const topArtists = Array.from(artistFreq.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 12) .map(x => x[0]); const scored = mixes.map(mix => { let score = 0; const tracks = mix.tracks || []; for (const tr of tracks) { const artists = tr.Artists || (tr.AlbumArtist ? [tr.AlbumArtist] : []); for (const a of artists) { if (!a) continue; const lower = a.toLowerCase(); if (topArtists.includes(lower)) score += 3; // strong match } // small bonus for track title matches to artist keywords if (tr.Name) { const name = tr.Name.toLowerCase(); for (const ta of topArtists) { if (name.includes(ta.split(' ')[0])) score += 0.25; } } } return { mix, score }; }); scored.sort((a, b) => b.score - a.score); return scored.map(s => s.mix); }; const getTrackImage = (track: MediaItem): string => { if (track.AlbumId && track.ImageTags?.Primary) { return api.getImageUrl(track, 'Primary', { width: 300, height: 300 }); } return '/default-thumbnail.png'; }; const closeMixQueue = () => { setShowMixQueue(null); setMixQueueTracks([]); setPlayingQueue(false); }; const handleDiscoverClick = () => { navigate('/discover'); }; // Only show loading if tracks are still loading if (tracksLoading) { return (

Loading your music library...

); } return (
{/* Dynamic Background */}
{backgroundImages.map((image, index) => (
))}
{/* Main Content */}
{/* Welcome Section */}

{getTimeOfDayGreeting()}

Your personal music universe awaits

{/* Quick Access Buttons */}
{/* Daily Mixes */}

Made for You

{loadingStates.mixes ? ( // Skeleton loading for mixes [...Array(3)].map((_, index) => (
)) ) : dailyMixes.length > 0 ? ( dailyMixes.map((mix) => (
handleMixTap(mix)} onDoubleClick={() => handleMixDoubleClick(mix)} >
{mix.name}
▶

{mix.name}

{mix.description}

)) ) : null}
{!loadingStates.mixes && dailyMixes.length > 0 && (

Tap to play • Double tap to view queue

)}
{/* Themed Daily Mixes - Toronto, Caribana, Holiday, Trending */}

🎭 Themed Mixes

{loadingStates.themedMixes ? 'Loading...' : `${themedMixes.length} themes`}

{loadingStates.themedMixes ? 'Loading Spotify themed playlists...' : 'Curated Spotify playlists available for discovery and download' }

{loadingStates.themedMixes ? ( // Skeleton loading for themed mixes [...Array(4)].map((_, index) => (
)) ) : themedMixes.length > 0 ? ( themedMixes.map((mix, index) => (
handleThemedMixPlay(mix)} onContextMenu={(e) => { e.preventDefault(); handleThemedMixLongPress(mix); }} onTouchStart={() => { touchTimerRef.current = window.setTimeout(() => handleThemedMixLongPress(mix), 600); }} onTouchEnd={() => { if (touchTimerRef.current) { clearTimeout(touchTimerRef.current); touchTimerRef.current = null; } }} onTouchCancel={() => { if (touchTimerRef.current) { clearTimeout(touchTimerRef.current); touchTimerRef.current = null; } }} >
{mix.title} { (e.target as HTMLImageElement).src = '/default-thumbnail.png'; }} /> {/* Download button for playlists */}
▶
{mix.theme === 'chill' && '😌'} {mix.theme === 'focus' && '🎯'} {mix.theme === 'local' && '🏙️'} {mix.theme === 'energy' && '⚡'} {mix.theme === 'discovery' && '💎'} {mix.theme === 'positive' && '☀️'} {mix.theme === 'toronto' && '🏙️'} {mix.theme === 'caribana' && '🎭'} {mix.theme === 'canadian' && '🍁'} {mix.theme === 'trendy' && '📱'} {(mix.theme === 'mixed' || !mix.theme) && '🎵'}

{mix.title}

{mix.description}

{mix.tracks.length} tracks
)) ) : (

🎵 Spotify themed playlists temporarily unavailable

)}
{!loadingStates.themedMixes && themedMixes.length > 0 && (

Tap to explore Spotify themed playlists • Available for download

)}
{/* Recently Played */}

Recently Played

{loadingStates.recentlyPlayed ? ( // Skeleton loading for recently played [...Array(6)].map((_, index) => (
)) ) : recentlyPlayed.length > 0 ? ( recentlyPlayed.map((track) => (
handleRecommendedTrackPlay(track)} >
{track.Name}
▶

{track.Name}

{track.Artists?.join(', ') || 'Unknown Artist'}

)) ) : null}
{/* Recommended for You */}

Recommended for You

{loadingStates.recommendations ? ( // Skeleton loading for recommendations [...Array(6)].map((_, index) => (
)) ) : recommendedTracks.length > 0 ? ( recommendedTracks.map((track) => (
handleRecommendedTrackPlay(track)} >
{track.Name}
▶

{track.Name}

{track.Artists?.join(', ') || 'Unknown Artist'}

Recommended for you
)) ) : null}
{/* Discover Preview - Made for You */}

🎵 {loadingStates.discover ? 'Discover' : discoverPreview?.title || 'Made for You'}

{loadingStates.discover ? 'Loading personalized recommendations...' : discoverPreview?.description || 'Explore new music' }

{loadingStates.discover ? ( // Better skeleton loading for discover
{[...Array(6)].map((_, index) => (
))}
) : discoverPreview && discoverPreview.tracks.length > 0 ? ( ) : discoverPreview ? (

🔄 {discoverPreview.title}

{discoverPreview.description}

) : (

🎵 Loading Discover content...

)}
{/* Mix Queue Overlay */} {showMixQueue && (
e.stopPropagation()}>

Queue Preview

{mixQueueTracks.slice(0, 10).map((track, index) => (
{index + 1} {track.Name}
{track.Name} {track.Artists?.join(', ') || 'Unknown Artist'}
))} {mixQueueTracks.length > 10 && (
+{mixQueueTracks.length - 10} more tracks
)}
)}
); }; const getTimeOfDayGreeting = (): string => { const hour = new Date().getHours(); // Dawn (5-6am) if (hour >= 5 && hour < 7) return 'Rise and Shine'; // Morning (7-11am) if (hour >= 7 && hour < 12) return 'Good Morning, Music Lover'; // Noon (12pm) if (hour === 12) return 'Midday Melodies'; // Afternoon (1-4pm) if (hour >= 13 && hour < 17) return 'Afternoon Vibes'; // Evening (5-7pm) if (hour >= 17 && hour < 20) return 'Evening Grooves'; // Sunset/Dusk (8-9pm) if (hour >= 20 && hour < 22) return 'Sunset Sessions'; // Night (10pm-12am) if (hour >= 22 || hour === 0) return 'Late Night Beats'; // Late Night (1-4am) if (hour >= 1 && hour < 5) return 'Midnight Mood'; // Default fallback return 'Welcome Back'; }; export default HomeEnhanced;