'use client' import { useState, useEffect, useRef, Suspense } from 'react' import { useSearchParams } from 'next/navigation' import { Button } from '@/components/ui/button' import { Badge } from '@/components/ui/badge' import { JefflixLogo } from '@/components/jefflix-logo' import { SongRow } from '@/components/music/search-results' import { useMusicPlayer, type Track } from '@/components/music/music-provider' import { MusicBrainzResultsSection } from '@/components/music/musicbrainz-results' import { useOffline } from '@/lib/stores/offline' import { Search, Music, Download, Loader2, ArrowLeft, AlertCircle, ListMusic, ChevronRight, ChevronDown, WifiOff, Users, DownloadCloud, CheckCircle } from 'lucide-react' import Link from 'next/link' interface Playlist { id: string name: string songCount: number coverArt: string } interface SlskdFile { displayName: string filename: string size: number bitRate: number length: number bestPeer: { username: string freeSlots: number speed: number } peerCount: number } export default function MusicPage() { return ( ) } function MusicPageInner() { const searchParams = useSearchParams() const { state } = useMusicPlayer() const { offlineIds, download: downloadTrack } = useOffline() const [query, setQuery] = useState(() => searchParams.get('q') || '') const [debouncedQuery, setDebouncedQuery] = useState('') const [songs, setSongs] = useState([]) const [searching, setSearching] = useState(false) const [searchError, setSearchError] = useState('') const debounceRef = useRef(null) // Playlist browsing state const [playlists, setPlaylists] = useState([]) const [playlistsLoading, setPlaylistsLoading] = useState(true) const [expandedPlaylist, setExpandedPlaylist] = useState(null) const [playlistSongs, setPlaylistSongs] = useState([]) const [playlistSongsLoading, setPlaylistSongsLoading] = useState(false) // Soulseek state const [slskMode, setSlskMode] = useState(false) const [slskSearchId, setSlskSearchId] = useState(null) const [slskResults, setSlskResults] = useState([]) const [slskSearching, setSlskSearching] = useState(false) const [downloading, setDownloading] = useState(null) const pollRef = useRef(null) // Fetch playlists on mount useEffect(() => { fetch('/api/music/playlists') .then((r) => r.json()) .then((d) => setPlaylists(d.playlists || [])) .catch(() => {}) .finally(() => setPlaylistsLoading(false)) }, []) const togglePlaylist = async (id: string) => { if (expandedPlaylist === id) { setExpandedPlaylist(null) setPlaylistSongs([]) return } setExpandedPlaylist(id) setPlaylistSongsLoading(true) try { const res = await fetch(`/api/music/playlist/${id}`) const d = await res.json() setPlaylistSongs(d.songs || []) } catch { setPlaylistSongs([]) } setPlaylistSongsLoading(false) } // Debounced Navidrome search useEffect(() => { if (slskMode) return debounceRef.current = setTimeout(() => setDebouncedQuery(query), 300) return () => { if (debounceRef.current) clearTimeout(debounceRef.current) } }, [query, slskMode]) useEffect(() => { if (!debouncedQuery || debouncedQuery.length < 2 || slskMode) { setSongs([]) return } setSearching(true) setSearchError('') fetch(`/api/music/search?q=${encodeURIComponent(debouncedQuery)}`) .then((r) => r.json()) .then((d) => { if (d.error) throw new Error(d.error) setSongs(d.songs || []) }) .catch((e) => setSearchError(e.message)) .finally(() => setSearching(false)) }, [debouncedQuery, slskMode]) // Cleanup slskd polling on unmount useEffect(() => { return () => { if (pollRef.current) clearTimeout(pollRef.current) } }, []) const searchSoulseek = async () => { setSlskMode(true) setSlskSearching(true) setSlskResults([]) try { const res = await fetch('/api/music/slskd/search', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ query: query || debouncedQuery }), }) const d = await res.json() if (d.error) throw new Error(d.error) setSlskSearchId(d.searchId) pollSlskResults(d.searchId) } catch { setSlskSearching(false) } } const pollSlskResults = (searchId: string) => { const poll = async () => { try { const res = await fetch(`/api/music/slskd/results/${searchId}`) const d = await res.json() setSlskResults(d.files || []) if (!d.isComplete) { pollRef.current = setTimeout(poll, 2000) } else { setSlskSearching(false) } } catch { setSlskSearching(false) } } poll() } const triggerDownload = async (file: SlskdFile) => { const key = `${file.bestPeer.username}:${file.filename}` setDownloading(key) try { await fetch('/api/music/slskd/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username: file.bestPeer.username, files: [{ filename: file.filename, size: file.size }], }), }) } catch {} setDownloading(null) } const exitSlsk = () => { setSlskMode(false) setSlskSearchId(null) setSlskResults([]) setSlskSearching(false) if (pollRef.current) clearTimeout(pollRef.current) } const hasPlayer = !!state.currentTrack return (
{/* Header */}
{/* Main */}
{/* Hero */}

Music

Search the library, play songs, and manage playlists.

{/* Search */}
{ setQuery(e.target.value); if (slskMode) exitSlsk() }} className="w-full pl-12 pr-4 py-3 rounded-lg border border-border bg-background focus:outline-none focus:ring-2 focus:ring-purple-500" placeholder="Search songs, artists, albums..." autoFocus />
{/* Soulseek mode toggle */} {slskMode && (
Soulseek Searching peer-to-peer network
)} {/* Navidrome Results */} {!slskMode && ( <> {searching && (
)} {searchError && (

{searchError}

)} {!searching && songs.length > 0 && (
{songs.map((song, i) => ( ))}
)} {!searching && debouncedQuery.length >= 2 && songs.length === 0 && !searchError && (

No results for “{debouncedQuery}” in the library

)} {query.length > 0 && query.length < 2 && (

Type at least 2 characters to search

)} {/* MusicBrainz Discovery */} {debouncedQuery.length >= 2 && ( )} )} {/* Soulseek Results */} {slskMode && ( <> {slskSearching && slskResults.length === 0 && (

Searching peer-to-peer network...

)} {slskResults.length > 0 && (
{slskSearching && (
Still searching...
)}
{slskResults.map((file) => { const sizeMB = (file.size / 1024 / 1024).toFixed(1) const key = `${file.bestPeer.username}:${file.filename}` return (
{file.displayName}
{sizeMB} MB {file.bitRate > 0 && ` · ${file.bitRate} kbps`} {' · '}{file.bestPeer.username}
{file.peerCount > 1 && ( {file.peerCount} )}
) })}
)} {!slskSearching && slskResults.length === 0 && slskSearchId && (

No results found on Soulseek

)} )} {/* Playlists - shown when not searching */} {!slskMode && !debouncedQuery && (

Playlists

{playlistsLoading ? (
) : playlists.length === 0 ? (

No playlists yet

) : (
{playlists.map((p) => { // Check how many songs in this playlist are already offline const allOffline = expandedPlaylist === p.id && playlistSongs.length > 0 && playlistSongs.every((s) => offlineIds.has(s.id)) return (
{/* Download entire playlist */} {expandedPlaylist === p.id && playlistSongs.length > 0 && ( )}
{expandedPlaylist === p.id && (
{playlistSongsLoading ? (
) : playlistSongs.length === 0 ? (

Empty playlist

) : (
{playlistSongs.map((song, i) => ( ))}
)}
)}
)})}
)}
)} {/* Info */}

How does this work?

This searches your Navidrome music library. Songs play directly in the browser through a persistent audio player. Can't find what you're looking for? Search Soulseek to find and download music from the peer-to-peer network.

) }