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 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 [recentlyPlayed, setRecentlyPlayed] = useState([]); const [recommendedTracks, setRecommendedTracks] = useState([]); const [discoverPreview, setDiscoverPreview] = useState(null); const [showMixQueue, setShowMixQueue] = useState(null); const [mixQueueTracks, setMixQueueTracks] = useState([]); const [lastTapTime, setLastTapTime] = useState>({}); // Loading states for individual sections const [loadingStates, setLoadingStates] = useState({ background: true, mixes: true, recentlyPlayed: true, recommendations: true, discover: true }); const instantMixService = InstantMixService.getInstance(); const imageRotationRef = 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(), 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, will retry with soft reset'); tryBackendSoftReset(); } } else { console.log('Home: Discover server responded with error:', response.status); // Try soft reset if we get a server error console.log('Home: Attempting soft reset...'); tryBackendSoftReset(); } } catch (error) { console.log('Home: Discover server error:', error); // Try soft reset on network errors console.log('Home: Attempting soft reset due to error...'); tryBackendSoftReset(); } finally { setLoadingStates(prev => ({ ...prev, discover: false })); } }; const tryBackendSoftReset = async () => { try { const backendUrl = import.meta.env.VITE_BACKEND_URL || 'http://localhost:5140'; const resetUrl = window.location.hostname === 'music.samcloud.ca' ? '/api/soft-reset' : `${backendUrl}/api/soft-reset`; console.log('Home: Attempting backend soft reset...'); await fetch(resetUrl, { method: 'POST' }); console.log('Home: Soft reset completed'); // Retry discover loading after a short delay setTimeout(() => { console.log('Home: Retrying discover after soft reset...'); loadDiscoverPreview(); }, 2000); } catch (error) { console.log('Home: Soft reset failed:', error); // If soft reset fails, show empty state with retry button setDiscoverPreview({ title: 'Discover Unavailable', description: 'Backend services are temporarily unavailable', tracks: [] }); } }; const loadDailyMixes = async () => { try { // Check for existing saved mixes const savedMixes = instantMixService.getSavedMixes(); if (savedMixes.length > 0) { 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 different 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' } ]; const generatedMixes: DailyMix[] = []; for (const mixConfig of mixTypes) { const seedTrack = allTracks[Math.floor(Math.random() * allTracks.length)]; const mixTracks = await instantMixService.generateInstantMix({ seedItem: seedTrack, mixType: mixConfig.type, trackCount: 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) => { const now = Date.now(); const lastTap = lastTapTime[mix.id] || 0; const timeDiff = now - lastTap; setLastTapTime(prev => ({ ...prev, [mix.id]: now })); // Double tap detection (within 300ms) if (timeDiff < 300) { // Double tap - show queue setMixQueueTracks(mix.tracks); setShowMixQueue(mix.id); } else { // Single tap - play mix after delay to allow for double tap setTimeout(() => { const currentLastTap = lastTapTime[mix.id] || 0; if (now === currentLastTap) { // No double tap occurred, play the mix setCurrentPlaylistSimple({ playlist: mix.tracks, title: mix.name }); playTrack(0); } }, 350); } }; 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 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([]); }; 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)} >
{mix.name}
▶

{mix.name}

{mix.description}

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

Tap to play • Double tap to view queue

)}
{/* 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;