"use client"; import { useState, useEffect, useRef } from "react"; import { useParams, useRouter } from "next/navigation"; import { ArrowLeft, Play, Pause, Download, Loader2, ExternalLink, Music2, Volume2, VolumeX, } from "lucide-react"; import { api } from "@/lib/api"; import { useToast } from "@/lib/toast-context"; import { cn } from "@/utils/cn"; import { GradientSpinner } from "@/components/ui/GradientSpinner"; // Deezer icon component const DeezerIcon = ({ className }: { className?: string }) => ( ); // Types for Deezer playlist interface DeezerTrack { deezerId: string; title: string; artist: string; artistId: string; album: string; albumId: string; durationMs: number; previewUrl: string | null; coverUrl: string | null; } interface DeezerPlaylistFull { id: string; title: string; description: string | null; creator: string; imageUrl: string | null; trackCount: number; tracks: DeezerTrack[]; isPublic: boolean; source: string; url: string; } export default function DeezerPlaylistDetailPage() { const params = useParams(); const router = useRouter(); const { toast } = useToast(); const playlistId = params.id as string; // State const [playlist, setPlaylist] = useState(null); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState(null); const [isImporting, setIsImporting] = useState(false); // Preview playback state const [playingTrackId, setPlayingTrackId] = useState(null); const [isPreviewPlaying, setIsPreviewPlaying] = useState(false); const [previewVolume, setPreviewVolume] = useState(0.5); const [isMuted, setIsMuted] = useState(false); const audioRef = useRef(null); // Fetch playlist data useEffect(() => { async function fetchPlaylist() { setIsLoading(true); setError(null); try { const data = await api.get( `/browse/playlists/${playlistId}` ); setPlaylist(data); } catch (err) { const message = err instanceof Error ? err.message : "Failed to load playlist"; setError(message); } finally { setIsLoading(false); } } fetchPlaylist(); }, [playlistId]); // Cleanup audio on unmount useEffect(() => { return () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } }; }, []); // Handle preview playback const handlePlayPreview = (track: DeezerTrack) => { if (!track.previewUrl) { toast.error("No preview available for this track"); return; } // If clicking the same track, toggle play/pause if (playingTrackId === track.deezerId) { if (isPreviewPlaying && audioRef.current) { audioRef.current.pause(); setIsPreviewPlaying(false); } else if (audioRef.current) { audioRef.current.play(); setIsPreviewPlaying(true); } return; } // Stop current preview if (audioRef.current) { audioRef.current.pause(); } // Play new preview const audio = new Audio(track.previewUrl); audio.volume = isMuted ? 0 : previewVolume; audioRef.current = audio; audio.onended = () => { setPlayingTrackId(null); setIsPreviewPlaying(false); }; audio.onerror = () => { toast.error("Failed to play preview"); setPlayingTrackId(null); setIsPreviewPlaying(false); }; audio.play(); setPlayingTrackId(track.deezerId); setIsPreviewPlaying(true); }; // Stop preview const stopPreview = () => { if (audioRef.current) { audioRef.current.pause(); audioRef.current = null; } setPlayingTrackId(null); setIsPreviewPlaying(false); }; // Toggle mute const toggleMute = () => { if (audioRef.current) { audioRef.current.volume = isMuted ? previewVolume : 0; } setIsMuted(!isMuted); }; // Handle import/download const handleImport = () => { if (!playlist) return; // Navigate to import page with the Deezer URL router.push( `/import/spotify?url=${encodeURIComponent(playlist.url)}` ); }; // Format duration const formatDuration = (ms: number) => { const minutes = Math.floor(ms / 60000); const seconds = Math.floor((ms % 60000) / 1000); return `${minutes}:${seconds.toString().padStart(2, "0")}`; }; // Calculate total duration const totalDuration = playlist?.tracks.reduce( (sum, track) => sum + track.durationMs, 0 ) || 0; const formatTotalDuration = (ms: number) => { const hours = Math.floor(ms / 3600000); const mins = Math.floor((ms % 3600000) / 60000); if (hours > 0) { return `about ${hours} hr ${mins} min`; } return `${mins} min`; }; if (isLoading) { return (
); } if (error || !playlist) { return (

Playlist not found

{error || "This playlist may be private or no longer available."}

); } return (
{/* Hero Section */}
{/* Cover Art */}
{playlist.imageUrl ? ( {playlist.title} ) : (
)}
{/* Playlist Info */}

Deezer Playlist

{playlist.title}

{playlist.description && (

{playlist.description}

)}
{playlist.creator} {playlist.trackCount} songs {totalDuration > 0 && ( <> , {formatTotalDuration(totalDuration)} )}
{/* Action Bar */}
{/* Download/Import Button */} {/* Volume Control (when playing preview) */} {playingTrackId && (
)} {/* Spacer */}
{/* Open in Deezer */} Open in Deezer
{/* Track Listing */}
{playlist.tracks.length > 0 ? (
{/* Table Header */}
# Title Album Duration
{/* Track Rows */}
{playlist.tracks.map((track, index) => { const isCurrentlyPlaying = playingTrackId === track.deezerId; const hasPreview = !!track.previewUrl; return (
hasPreview && handlePlayPreview(track) } className={cn( "grid grid-cols-[40px_1fr_auto] md:grid-cols-[40px_minmax(200px,4fr)_minmax(100px,1fr)_80px] gap-4 px-4 py-2 rounded-md transition-colors group", hasPreview ? "hover:bg-white/5 cursor-pointer" : "opacity-60 cursor-not-allowed", isCurrentlyPlaying && "bg-white/10" )} > {/* Track Number / Play Icon */}
{hasPreview ? ( <> {isCurrentlyPlaying && isPreviewPlaying ? ( ) : ( index + 1 )} ) : ( {index + 1} )}
{/* Title + Artist */}
{track.coverUrl ? ( {track.title} ) : (
)}

{track.title}

{track.artist}

{/* Album (hidden on mobile) */}

{track.album}

{/* Duration */}
{formatDuration(track.durationMs)}
); })}
) : (

No tracks found

This playlist appears to be empty

)}
{/* Preview indicator */} {playingTrackId && (
Playing 30s preview
)}
); }