import { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { Album, ExternalLink, Heart, LoaderCircle, Mic2, Play, Podcast, } from "lucide-react"; import { Button } from "../components/Button"; import { DownloadButton } from "../components/DownloadButton"; import { useSpotifyDownload } from "../helpers/useSpotifyDownload"; import { LyricsPanel } from "../components/LyricsPanel"; import { Tabs, TabsList, TabsTrigger, } from "../components/Tabs"; import { SpotifyPlayerBar } from "../components/SpotifyPlayerBar"; import { getSpotifyLibrary, type SpotifyLibraryItem, type SpotifyLibrarySection, } from "../endpoints/spotify-library_GET.schema"; import { getSpotifySession, type OutputType as SpotifySession, } from "../endpoints/spotify-session_GET.schema"; import { postLyrics, type OutputType as LyricsResult } from "../endpoints/lyrics_POST.schema"; import { useSpotifyPlayer } from "../helpers/useSpotifyPlayer"; import styles from "./spotify-library.module.css"; import { MobileNav } from "../components/MobileNav"; const labels: Record = { tracks: "Liked Songs", albums: "Saved Albums", artists: "Followed Artists", shows: "Saved Shows", }; export default function SpotifyLibraryPage() { const [spotify, setSpotify] = useState({ connected: false }); const [section, setSection] = useState("tracks"); const [items, setItems] = useState([]); const [total, setTotal] = useState(); const [nextCursor, setNextCursor] = useState(); const [loading, setLoading] = useState(false); 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(); useEffect(() => { getSpotifySession() .then((session) => { setSpotify(session); if (session.connected) void loadSection("tracks", undefined, false); }) .catch((reason) => setError(reason instanceof Error ? reason.message : "Unable to read Spotify connection."), ); }, []); useEffect(() => { if (!lyricsOpen || !player.track?.title || !player.track.artist) return; let cancelled = false; setLyricsLoading(true); setLyrics(undefined); setLyricsError(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 loadSection( target: SpotifyLibrarySection, cursor?: string, append = false, ) { setLoading(true); setError(undefined); try { const result = await getSpotifyLibrary(target, cursor); setItems((current) => append ? [...current, ...result.items] : result.items); setTotal(result.total); setNextCursor(result.nextCursor); } catch (reason) { setError(reason instanceof Error ? reason.message : "Unable to load Spotify library."); if (!append) { setItems([]); setTotal(undefined); setNextCursor(undefined); } } finally { setLoading(false); } } function changeSection(value: string) { const target = value as SpotifyLibrarySection; setSection(target); setItems([]); setNextCursor(undefined); setTotal(undefined); void loadSection(target, undefined, false); } async function play(item: SpotifyLibraryItem) { if (!item.uri || item.type !== "track") return; try { setError(undefined); await player.playUri(item.uri); } catch (reason) { setError(reason instanceof Error ? reason.message : "Unable to start Spotify playback."); } } return (
My Spotify
SPOTIFY LIBRARY

My Spotify

Browse the music and shows saved to your connected Spotify account without copying URLs.

{!spotify.connected ? ( ) : (
{spotify.profile?.imageUrl ? : null}
{spotify.profile?.displayName || "Spotify"}{spotify.profile?.product || "account"}
)}
{error ? (
{error} {spotify.connected ? : null}
) : null}
Liked Songs Albums Artists Shows
{typeof total === "number" ? total + " total" : labels[section]}
{section === "tracks" ? (
#TitleAlbumAdded
{items.map((item, index) => (
{String(index + 1).padStart(2, "0")}
{item.imageUrl ? : }
{item.name}{item.subtitle}
{item.detail || "—"} {item.addedAt ? new Date(item.addedAt).toLocaleDateString() : "—"}
{item.externalUrl ? ( ) : null} {item.externalUrl ? ( ) : null}
))}
) : (
{items.map((item) => (
{item.imageUrl ? : section === "artists" ? : section === "shows" ? : }
{item.name} {item.subtitle || item.detail || "Spotify"} {item.total ? {item.total} {section === "shows" ? "episodes" : "tracks"} : null}
{section === "albums" ? ( ) : null} {section === "albums" && item.externalUrl ? ( ) : null} {section === "artists" ? ( ) : null} {item.externalUrl ? ( ) : null}
))}
)} {!items.length && !loading && spotify.connected && !error ? (
No items found in {labels[section]}.
) : null} {loading ? (
Loading {labels[section].toLowerCase()}…
) : null} {nextCursor && !loading ? (
) : null}
{spotify.connected ? (
setLyricsOpen((open) => !open)} onTogglePlay={() => player.togglePlay()} onPrevious={() => player.previousTrack()} onNext={() => player.nextTrack()} onSeek={(position) => player.seek(position)} onVolume={(volume) => player.setVolume(volume)} />
) : null} {lyricsOpen ? ( setLyricsOpen(false)} /> ) : null}
); }