import React, { useState, useEffect, useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; import { MediaList } from '../components/MediaList'; import { Loader } from '../components/Loader'; import { PullToRefreshWrapper } from '../components/PullToRefreshWrapper'; import { useSocket } from '../hooks/useSocket'; import { useDownloadContext } from '../context/DownloadContext/DownloadContext'; // import { usePageTitle } from '../hooks/usePageTitle'; // Commented out - hook not found import { MediaItem } from '../api/jellyfin'; import { toast } from 'sonner'; import { Music, Heart, Star, TrendingUp, Radio, Disc3, RotateCcw } from 'lucide-react'; 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; } type TabType = 'all' | 'made-for-you' | 'new-releases' | 'trending' | 'genres' | 'albums'; interface Tab { id: TabType; label: string; icon: React.ComponentType<{ size?: number; className?: string }>; } const TABS: Tab[] = [ { id: 'all', label: 'All', icon: Music }, { id: 'made-for-you', label: 'Made for You', icon: Heart }, { id: 'new-releases', label: 'New Releases', icon: Star }, { id: 'trending', label: 'Trending', icon: TrendingUp }, { id: 'genres', label: 'Genres', icon: Radio }, { id: 'albums', label: 'Albums', icon: Disc3 } ]; export const Discover: React.FC = () => { // const { setPageTitle } = usePageTitle(); // Commented out - hook not found const [discoveryData, setDiscoveryData] = useState({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [activeTab, setActiveTab] = useState('all'); const [downloadStatus, setDownloadStatus] = useState<{ [key: string]: any }>({}); const navigate = useNavigate(); const { socket } = useSocket(); const { downloads, updateDownloadStatus } = useDownloadContext(); // Base path for saving downloads directly into Jellyfin library // const MUSIC_BASE_PATH = process.env.VITE_MUSIC_BASE_PATH || '/Volumes/Jellyfin Media/Music'; // Removed unused variable // const sanitizeFilename = (name: string) => { // if (!name) return 'Unknown'; // return name // .replace(/[\\/:*?"<>|]/g, '-') // .replace(/\s+/g, ' ') // .trim(); // }; // Removed unused function // Universal download handler for Discover page items const handleDiscoverDownload = (item: MediaItem) => { if (!socket) { toast.error('Not connected to the server.'); return; } // Extract Spotify data from the MediaItem const spotifyId = (item as any).SpotifyId; // const spotifyUrl = (item as any).SpotifyUrl; // Removed unused variable if (!spotifyId) { toast.error('Invalid item - missing Spotify ID.'); return; } console.log('🔽 Starting Discover download for:', item.Name, 'Spotify ID:', spotifyId); // Handle album downloads if (item.Type === 'MusicAlbum' || (item as any).MediaType === 'Album') { console.log('🎵 Starting album download for:', item.Name, 'ID:', spotifyId); socket.emit('download_spotify_album', { albumId: spotifyId, albumName: item.Name }); toast.info(`Started downloading album "${item.Name}"`); return; } // Handle track downloads toast.info(`Downloading ${item.Name}...`); // Build target path: Artist/Album (variables removed as unused) // const sanitizedTitle = sanitizeFilename(item.title || item.name || 'Unknown'); // const sanitizedArtist = sanitizeFilename(item.artist || 'Unknown Artist'); // const targetPath = `${MUSIC_BASE_PATH}/${sanitizedArtist}/${sanitizedTitle}`; // Removed unused variable // Use the Spotify-specific download endpoint socket.emit('download_spotify', { spotifyId: spotifyId, trackName: item.Name, artistName: item.AlbumArtist || item.Artists?.[0] || 'Unknown Artist' }); // Update local download status for immediate UI feedback const backendKey = `spotify_${spotifyId}`; const downloadInfo = { status: 'Queued...', progress: 0, id: backendKey, name: item.Name }; setDownloadStatus((prevStatus: any) => ({ ...prevStatus, [spotifyId]: downloadInfo, [backendKey]: downloadInfo })); // Also update the DownloadContext immediately for UI feedback if (updateDownloadStatus) { updateDownloadStatus(backendKey, 'accepted', 0); } }; // Transform discovery items to include download status and Spotify metadata const transformDiscoverItems = (items: MediaItem[]) => { return items.map(item => { const spotifyId = (item as any).SpotifyId || (item as any).spotify_id; // Download path will be determined by backend // Get download status from either local state or context const localStatus = downloadStatus[spotifyId] || downloadStatus[spotifyId]; const contextStatus = downloads[spotifyId]; // Determine offline state based on download status let offlineState = 'none'; let downloadProgress = 0; if (localStatus || contextStatus) { const activeStatus = localStatus || contextStatus; if (activeStatus.status === 'completed' || activeStatus.status === 'finished') { offlineState = 'downloaded'; } else if (activeStatus.status === 'downloading' || activeStatus.status === 'accepted' || activeStatus.status === 'Queued...') { offlineState = 'downloading'; downloadProgress = activeStatus.progress || 0; } } return { ...item, // Ensure Spotify metadata is preserved SpotifyId: spotifyId, SpotifyUrl: (item as any).SpotifyUrl || (item as any).spotify_url, SpotifyImage: (item as any).SpotifyImage || (item as any).spotify_image, // Add download status offlineState, downloadProgress } as MediaItem; }); }; // Socket event handlers for download progress useEffect(() => { if (!socket) return; const handleDownloadUpdate = (data: any) => { console.log('🔄 Discover download update received:', data); // Handle the backend's data structure: {history: [array_of_downloads]} if (data.history && Array.isArray(data.history)) { const newStatus: { [key: string]: any } = {}; data.history.forEach((download: any) => { const key = download.id || download.spotify_id; if (key) { newStatus[key] = { status: download.status, progress: download.progress || 0, id: key, name: download.name || download.track_name }; } }); setDownloadStatus((prevStatus: any) => ({ ...prevStatus, ...newStatus })); } }; socket.on('update_status', handleDownloadUpdate); return () => { socket.off('update_status', handleDownloadUpdate); }; }, [socket]); const fetchDiscoveryData = useCallback(async (forceRefresh = false) => { setLoading(true); setError(null); try { // Check if we need to refresh based on cache timestamps const lastFetch = localStorage.getItem('discover_last_fetch'); const lastFetchTime = lastFetch ? parseInt(lastFetch) : 0; const now = Date.now(); const hoursSinceLastFetch = (now - lastFetchTime) / (1000 * 60 * 60); // Use cached data if it's less than 2 hours old and not forcing refresh if (!forceRefresh && hoursSinceLastFetch < 2 && localStorage.getItem('discover_cache')) { console.log('Using cached discovery data (less than 1 hour old)'); const cachedData = JSON.parse(localStorage.getItem('discover_cache')!); setDiscoveryData(cachedData); setLoading(false); return; } // 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 as any).env?.VITE_BACKEND_URL || 'http://localhost:5140'; fetchUrl = `${backendUrl}/api/discover`; } console.log('Environment:', window.location.hostname); console.log('Fetching fresh discovery data from:', fetchUrl); // Add cache-busting parameter to get fresh data const cacheBustUrl = `${fetchUrl}?t=${now}`; const response = await fetch(cacheBustUrl); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.error || `HTTP error! status: ${response.status}`); } const data = await response.json(); // Cache the fresh data localStorage.setItem('discover_cache', JSON.stringify(data)); localStorage.setItem('discover_last_fetch', now.toString()); console.log('Discovery data received and cached:', data); console.log('For you tracks count:', data.for_you?.tracks?.length || 0); console.log('Trending tracks count:', data.trending?.tracks?.length || 0); console.log('New releases tracks count:', data.new_releases?.tracks?.length || 0); console.log('Genres count:', data.genres?.length || 0); setDiscoveryData(data); } catch (e: unknown) { console.error("Failed to fetch discovery data:", e); setError((e as Error).message || 'Failed to fetch discovery content.'); } finally { setLoading(false); } }, []); const handleRefresh = async () => { console.log('Manual refresh triggered - clearing cache'); // Clear cache to force fresh data localStorage.removeItem('discover_cache'); localStorage.removeItem('discover_last_fetch'); await fetchDiscoveryData(true); }; useEffect(() => { // setPageTitle('Discover'); // Commented out - hook not found fetchDiscoveryData(); // Set up auto-refresh every 2 hours for more current content const refreshInterval = setInterval(() => { console.log('🔄 Auto-refreshing discovery data (2 hour interval)'); fetchDiscoveryData(true); // Force refresh for hourly updates }, 2 * 60 * 60 * 1000); // 2 hours return () => clearInterval(refreshInterval); }, [fetchDiscoveryData]); if (loading) { return (
); } if (error) { return ( fetchDiscoveryData(true)}>

Failed to load discovery content

{error}

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

No recommendations available

Start listening to music to get personalized recommendations!

); } // Render tab content based on active tab const renderTabContent = () => { if (loading) { return (

Loading {TABS.find(t => t.id === activeTab)?.label}...

); } if (error) { return (

Unable to load content

{error}

); } switch (activeTab) { case 'all': return renderAllContent(); case 'made-for-you': return renderMadeForYouContent(); case 'new-releases': return renderNewReleasesContent(); case 'trending': return renderTrendingContent(); case 'genres': return renderGenresContent(); case 'albums': return renderAlbumsContent(); default: return renderAllContent(); } }; const renderAllContent = () => { return (
{/* Made for You Section */} {discoveryData.for_you && discoveryData.for_you.tracks && discoveryData.for_you.tracks.length > 0 && (
{discoveryData.for_you.title}
{discoveryData.for_you.description}
navigate('/section/for-you?limit=100')}> See more (100)
)} {/* New Releases Section */} {discoveryData.new_releases && discoveryData.new_releases.tracks.length > 0 && (
{discoveryData.new_releases.title}
{discoveryData.new_releases.description}
navigate('/section/new-releases?limit=100')}> See more (100)
)} {/* Trending Section */} {discoveryData.trending && discoveryData.trending.tracks.length > 0 && (
{discoveryData.trending.title}
{discoveryData.trending.description}
navigate('/section/trending?limit=100')}> See more (100)
)} {/* Trending Albums Section */} {discoveryData.trending_albums && discoveryData.trending_albums.albums.length > 0 && (
{discoveryData.trending_albums.title}
{discoveryData.trending_albums.description}
navigate('/section/trending-albums?limit=100')}> See more (100)
)} {/* New Albums Section */} {discoveryData.new_albums && discoveryData.new_albums.albums.length > 0 && (
New Albums Release
Fresh albums just for you
navigate('/section/new-albums?limit=100')}> See more (100)
)} {/* Genre Sections */} {discoveryData.genres && discoveryData.genres.length > 0 && ( <>
Explore by Genre
Discover new music across different genres
navigate('/section/genres?limit=100')}> See more (100)
{discoveryData.genres.slice(0, 2).map((genreSection: GenreSection, index: number) => (
{genreSection.title}
Explore {genreSection.genre} music
navigate(`/section/genre-${encodeURIComponent(genreSection.genre)}?limit=100`)}> See more (100)
))} )} {/* Show message if no content is available */} {!discoveryData.for_you?.tracks?.length && !discoveryData.new_releases?.tracks?.length && !discoveryData.trending?.tracks?.length && !discoveryData.trending_albums?.albums?.length && !discoveryData.new_albums?.albums?.length && !discoveryData.genres?.length && (

No discovery content available

Check your backend connection or try refreshing the page.

)}
); }; const renderMadeForYouContent = () => { if (!discoveryData.for_you || !discoveryData.for_you.tracks || discoveryData.for_you.tracks.length === 0) { return (

No personalized recommendations yet

Start listening to music to get recommendations based on your taste and current song!

); } return (

{discoveryData.for_you.title}

{discoveryData.for_you.description}

navigate('/section/for-you?limit=100')}> See all 100 recommendations
); }; const renderNewReleasesContent = () => { if (!discoveryData.new_releases || !discoveryData.new_releases.tracks || discoveryData.new_releases.tracks.length === 0) { return (

No new releases available

Check back later for the latest music releases!

); } return (

{discoveryData.new_releases.title}

{discoveryData.new_releases.description}

navigate('/section/new-releases?limit=100')}> See all 100 new releases
); }; const renderTrendingContent = () => { if (!discoveryData.trending || discoveryData.trending.tracks.length === 0) { return (

No trending tracks available

Check back later for trending music!

); } return (

{discoveryData.trending.title}

{discoveryData.trending.description}

navigate('/section/trending?limit=100')}> See all 100 trending tracks
); }; const renderGenresContent = () => { if (!discoveryData.genres || discoveryData.genres.length === 0) { return (

No genre recommendations available

Check back later for genre-based recommendations!

); } return (

Explore by Genre

Discover new music across different genres

{discoveryData.genres.map((genreSection: GenreSection, index: number) => (
{genreSection.title}
Explore {genreSection.genre} music
navigate(`/section/genre-${encodeURIComponent(genreSection.genre)}?limit=50`)}> See more (50)
))}
); }; const renderAlbumsContent = () => { const hasAlbums = (discoveryData.trending_albums?.albums.length || 0) > 0 || (discoveryData.new_albums?.albums.length || 0) > 0; if (!hasAlbums) { return (

No album recommendations available

Check back later for album recommendations!

); } return (

Album Discoveries

Explore trending and new album releases

{/* Trending Albums */} {discoveryData.trending_albums && discoveryData.trending_albums.albums.length > 0 && (
{discoveryData.trending_albums.title}
{discoveryData.trending_albums.description}
navigate('/section/trending-albums?limit=100')}> See more (100)
)} {/* New Albums */} {discoveryData.new_albums && discoveryData.new_albums.albums.length > 0 && (
New Albums Release
Fresh albums just for you
navigate('/section/new-albums?limit=100')}> See more (100)
)}
); }; return (
{/* Tab Navigation */}
{TABS.map((tab) => ( ))}
{/* Tab Content */}
{renderTabContent()}
); };