import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { AudioLines, ChevronLeft, ChevronRight, Download, ExternalLink, FolderOpen, ListMusic, Loader2, Music2, Settings2, Unplug, } from "lucide-react"; import { Button } from "../components/Button"; import { Input } from "../components/Input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "../components/Select"; import { SpotifyPlayerBar } from "../components/SpotifyPlayerBar"; import { LyricsPanel } from "../components/LyricsPanel"; import { MobileNav } from "../components/MobileNav"; import { getSpotifyPlaylists, type SpotifyPlaylistSummary, } from "../endpoints/spotify-playlists_GET.schema"; import { getSpotifySession, type OutputType as SpotifySession } from "../endpoints/spotify-session_GET.schema"; import { postSpotifyLogout } from "../endpoints/spotify-logout_POST.schema"; import { postSpotifyPlaylist, type OutputType as SpotifyPlaylist } from "../endpoints/spotify-playlist_POST.schema"; import { postLyrics, type OutputType as LyricsResult } from "../endpoints/lyrics_POST.schema"; import { loadOutputDirectory, saveOutputDirectory, } from "../helpers/directoryStore"; import type { WritableDirectoryHandle } from "../helpers/mp3Tags"; import { useSpotifyPlayer } from "../helpers/useSpotifyPlayer"; import { useSpotifyDownload } from "../helpers/useSpotifyDownload"; import { addLibraryHistory } from "../helpers/libraryHistory"; import { findDuplicateIdentities } from "../helpers/libraryIdentity"; import styles from "./_index.module.css"; type Track = { id: number; title: string; artist: string; album: string; duration: string; durationMs: number; uri?: string; imageUrl?: string; externalUrl?: string; isLocal?: boolean; isrc?: string; duplicate?: boolean; }; const placeholderTracks: Track[] = [ { id: 1, title: "Connect Spotify", artist: "Then paste a playlist URL above", album: "Aurora SpotiRip", duration: "0:00", durationMs: 0, }, ]; function formatTime(ms: number) { const total = Math.max(0, Math.floor(ms / 1000)); return `${Math.floor(total / 60)}:${String(total % 60).padStart(2, "0")}`; } export default function HomePage() { const [playlistUrl, setPlaylistUrl] = useState(""); const [directoryName, setDirectoryName] = useState("Music / Aurora Exports"); const [tracks, setTracks] = useState(placeholderTracks); const [spotify, setSpotify] = useState({ connected: false }); const [playlist, setPlaylist] = useState(); const [availablePlaylists, setAvailablePlaylists] = useState([]); const [trackPage, setTrackPage] = useState(0); const [trackFilter, setTrackFilter] = useState(""); const [loadingPlaylist, setLoadingPlaylist] = useState(false); const [message, setMessage] = useState(); const [error, setError] = useState(); const [lyricsOpen, setLyricsOpen] = useState(false); const [lyricsLoading, setLyricsLoading] = useState(false); const [lyrics, setLyrics] = useState(); const [lyricsError, setLyricsError] = useState(); const player = useSpotifyPlayer(spotify.accessToken); const downloads = useSpotifyDownload(); const canDownload = Boolean(spotify.connected && spotify.canDownload); const downloadableTracks = tracks.filter((track) => track.externalUrl && !track.isLocal); const pageSize = 20; const normalizedFilter = trackFilter.trim().toLowerCase(); const filteredTracks = normalizedFilter ? tracks.filter((track) => [track.title, track.artist, track.album].some((value) => value.toLowerCase().includes(normalizedFilter), ), ) : tracks; const pageCount = Math.max(1, Math.ceil(filteredTracks.length / pageSize)); const visibleTracks = filteredTracks.slice( trackPage * pageSize, trackPage * pageSize + pageSize, ); useEffect(() => { getSpotifySession() .then(async (session) => { setSpotify(session); if (session.connected) { try { const result = await getSpotifyPlaylists(); setAvailablePlaylists( result.playlists.filter( (entry) => entry.ownerId === session.profile?.id || entry.collaborative, ), ); } catch (reason) { setError( reason instanceof Error ? reason.message : "Unable to list your Spotify playlists.", ); } } }) .catch((reason) => setError( reason instanceof Error ? reason.message : "Unable to read Spotify connection.", ), ); }, []); useEffect(() => { loadOutputDirectory() .then((handle) => { if (handle?.name) setDirectoryName(handle.name); }) .catch(() => undefined); }, []); useEffect(() => { if (!lyricsOpen || !player.track?.title || !player.track.artist) return; let cancelled = false; setLyricsLoading(true); setLyricsError(undefined); setLyrics(undefined); postLyrics({ title: player.track.title, artist: player.track.artist, album: player.track.album || undefined, }) .then((result) => { if (!cancelled) setLyrics(result); }) .catch((reason) => { if (!cancelled) { setLyricsError(reason instanceof Error ? reason.message : "Unable to load lyrics."); } }) .finally(() => { if (!cancelled) setLyricsLoading(false); }); return () => { cancelled = true; }; }, [lyricsOpen, player.track?.uri, player.track?.title, player.track?.artist, player.track?.album]); async function chooseFolder() { const picker = (window as typeof window & { showDirectoryPicker?: () => Promise; }).showDirectoryPicker; if (!picker) { setDirectoryName("Browser downloads folder"); return; } try { const handle = await picker(); setDirectoryName(handle.name || "Selected folder"); await saveOutputDirectory(handle); } catch { } } async function importPlaylist(source = playlistUrl) { const target = source.trim(); if (!target) return; if (!spotify.connected) { setError("Connect Spotify before importing a playlist."); return; } setLoadingPlaylist(true); setError(undefined); setMessage(undefined); try { const imported = await postSpotifyPlaylist({ playlistUrl: target }); const duplicateIndexes = findDuplicateIdentities( imported.tracks.map((track) => ({ isrc: track.isrc, title: track.title, artist: track.artist, album: track.album, })), ); setPlaylist(imported); setTrackPage(0); setTracks( imported.tracks.map((track, index) => ({ id: index + 1, title: track.title, artist: track.artist, album: track.album, duration: formatTime(track.durationMs), durationMs: track.durationMs, uri: track.uri, imageUrl: track.imageUrl, externalUrl: track.externalUrl, isLocal: track.isLocal, isrc: track.isrc, duplicate: duplicateIndexes.has(index), })), ); setMessage(`Loaded ${imported.tracks.length} tracks from “${imported.name}”.`); setPlaylistUrl(target); addLibraryHistory({ kind: "playlist", title: imported.name, trackCount: imported.tracks.length, artworkUrl: imported.imageUrl, sourceUrl: imported.externalUrl, }); } catch (reason) { setError(reason instanceof Error ? reason.message : "Playlist import failed."); } finally { setLoadingPlaylist(false); } } async function disconnectSpotify() { try { await postSpotifyLogout(); setSpotify({ connected: false }); setPlaylist(undefined); setAvailablePlaylists([]); setTracks(placeholderTracks); setTrackPage(0); setMessage("Spotify disconnected."); setError(undefined); } catch (reason) { setError(reason instanceof Error ? reason.message : "Unable to disconnect Spotify."); } } async function playTrack(track: Track) { if (!track.uri) return; try { setError(undefined); await player.playUri(track.uri); } catch (reason) { setError(reason instanceof Error ? reason.message : "Unable to start Spotify playback."); } } return (
PLAYLIST WORKBENCH

Library Queue

{spotify.connected ? (
{spotify.profile?.imageUrl ? : null}
{spotify.profile?.displayName || "Spotify connected"} {spotify.profile?.product === "premium" ? "Premium" : spotify.profile?.product || "Spotify"}
) : ( )}
SPOTIFY SOURCE Import a playlist
{spotify.connected && availablePlaylists.length ? ( ) : null} setPlaylistUrl(event.target.value)} placeholder="Paste Spotify playlist URL" aria-label="Spotify playlist URL" />
{playlist ? playlist.total : 0} tracks {player.ready ? "Player ready" : spotify.connected ? "Connecting player" : "Spotify offline"}
{(error || message || player.error) && (
{error || player.error || message}
)}
SPOTIFY PLAYLIST

{playlist?.name || "Import a playlist"}

{playlist?.description || "Connect Spotify, paste a playlist URL, and SpotiRip will load the real track list and artwork here."}

{playlist?.externalUrl ? ( Spotify OPEN SPOTIFY ) : null}
{playlist ? playlist.tracks.length : 0}Tracks
{playlist?.owner || "—"}Owner
{player.ready ? "Ready" : "—"}Web player
QUICK ACTIONS
Audio toolsIdentify unknown files
Audio toolsConvert local audio to MP3
CURRENT QUEUE {playlist?.name || "No playlist loaded"} {playlist?.owner ? by {playlist.owner} : null}
{ setTrackFilter(event.target.value); setTrackPage(0); }} placeholder="Filter tracks" aria-label="Filter playlist tracks" /> {canDownload && playlist && downloadableTracks.length > 0 ? ( ) : null}
# Title Album Time Status
{visibleTracks.map((track) => (
playTrack(track)} onKeyDown={(event) => { if (event.key === "Enter" || event.key === " ") { event.preventDefault(); playTrack(track); } }} > {String(track.id).padStart(2, "0")}
{track.imageUrl ? : }
{track.title} {track.artist}
{track.album} {track.duration}
{track.duplicate ? "Duplicate" : track.isLocal ? "Local item" : track.isrc ? "ISRC" : "Spotify"} {canDownload && track.externalUrl && !track.isLocal ? ( ) : null} {track.externalUrl ? ( event.stopPropagation()} > ) : null}
))}
{playlist && pageCount > 1 ? (
Page {trackPage + 1} of {pageCount}
) : null} {playlist && filteredTracks.length === 0 ? (
No tracks match “{trackFilter}”.
) : null}
{spotify.connected ? "Spotify connected" : "Spotify disconnected"} {directoryName} ID3 + filename normalization
setLyricsOpen((open) => !open)} onTogglePlay={() => player.togglePlay()} onPrevious={() => player.previousTrack()} onNext={() => player.nextTrack()} onSeek={(position) => player.seek(position)} onVolume={(volume) => player.setVolume(volume)} />
{lyricsOpen ? ( setLyricsOpen(false)} /> ) : null}
); }