'use client' import { useState, useEffect, useRef } from 'react' 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 { Search, Music, Download, Loader2, ArrowLeft, AlertCircle } from 'lucide-react' import Link from 'next/link' interface SlskdResult { username: string freeSlots: number speed: number files: { filename: string size: number bitRate: number length: number }[] } export default function MusicPage() { const { state } = useMusicPlayer() const [query, setQuery] = useState('') const [debouncedQuery, setDebouncedQuery] = useState('') const [songs, setSongs] = useState([]) const [searching, setSearching] = useState(false) const [searchError, setSearchError] = useState('') const debounceRef = useRef(null) // 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) // 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.results || []) if (!d.isComplete) { pollRef.current = setTimeout(poll, 2000) } else { setSlskSearching(false) } } catch { setSlskSearching(false) } } poll() } const triggerDownload = async (username: string, files: SlskdResult['files']) => { const key = `${username}:${files[0]?.filename}` setDownloading(key) try { await fetch('/api/music/slskd/download', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ username, files }), }) } 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

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

Searching peer-to-peer network...

)} {slskResults.length > 0 && (
{slskSearching && (
Still searching...
)} {slskResults.map((result) => (
{result.username} {result.freeSlots > 0 ? `${result.freeSlots} free slots` : 'No free slots'}
{result.files.map((file) => { const name = file.filename.split('\\').pop() || file.filename const sizeMB = (file.size / 1024 / 1024).toFixed(1) const key = `${result.username}:${file.filename}` return (
{name}
{sizeMB} MB {file.bitRate > 0 && ` ยท ${file.bitRate} kbps`}
) })}
))}
)} {!slskSearching && slskResults.length === 0 && slskSearchId && (

No results found on Soulseek

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

) }