diff --git a/src/App.tsx b/src/App.tsx index 77c2262..ea05577 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react' +import { useEffect, useRef, useState } from 'react' import { MusicBrainzApi, type ITrack } from 'musicbrainz-api'; import Album from "./components/Album/Album"; import AlbumArt from "./components/AlbumArt/AlbumArt"; @@ -28,7 +28,7 @@ async function getCoverArtSrcURL(releaseId: string, releaseGroupId: string): Pro continue; } const data = await res.json(); - const front = (data.images || []).find(img => img.front) || data.images?.[0]; + const front = (data.images || []).find((img: any) => img.front) || data.images?.[0]; if (front) { return front.thumbnails?.["1200"]|| front.image; } @@ -55,11 +55,48 @@ function navidromeURL(path: string, params: Record): string { function App() { const [album, setAlbum] = useState(null); const [buffer, setBuffer] = useState([]); - const [nowPlaying, play] = useState(null); + const [nowPlaying, play] = useState(0); const [searching, setSearching] = useState(false); - const [paused, setPaused] = useState(false); + const [paused, setPaused] = useState(true); + const [currentTime, setCurrentTime] = useState(0); + const [duration, setDuration] = useState(0); + const audioRef = useRef(null); - // Handle input + const handleEnded = () => { + if ((album?.tracks?.length ?? 0) > nowPlaying + 1) { // if there's a next track + play(nowPlaying + 1); + setPaused(false); + } + }; + + // When a track ends or is skipped, play the new track + useEffect(() => { + const audio = audioRef.current; + if (!audio) { + return; + } + audio.currentTime = 0; + audio.load(); + setCurrentTime(0); + setDuration(0); + audio.play().catch(() => setPaused(true)); + }, [nowPlaying]); + + // Handle play/pause + useEffect(() => { + const audio = audioRef.current; + if (!audio) { + return; + } + if (paused) { + audio.pause(); + } else { + audio.play().catch(() => setPaused(true)); + } + }, [paused]); + + + // Handle keyboard/scanner input useEffect(() => { const handleKeyDown = (ev: KeyboardEvent) => { if (ev.key === "s") { // TODO: remove @@ -119,7 +156,7 @@ function App() { const ndRes = await fetch(navidromeURL('/rest/getAlbum.view', { id: ndFoundAlbum.id })).then(res => res.json()); const ndAlbum = ndRes["subsonic-response"].album; console.log("Found album in Navidrome:", ndAlbum); - let tracks: PlayableTrack[] = ndAlbum.song?.map((t: any) => ({ + let tracks: Track[] = ndAlbum.song?.map((t: any) => ({ title: t.title, duration: t.duration, id: t.id, @@ -132,10 +169,10 @@ function App() { releaseDate: ndAlbum.originalReleaseDate??'', tracks, coverArtLink: navidromeURL('/rest/getCoverArt.view', { id: ndAlbum.coverArt }), - }) + }); // # …and play! - // play(album.tracks[0]); - // TODO + play(0); + setPaused(false); setSearching(false); } else { // Not found in Navidrome; populate with MusicBrainz data instead const fullMbRelease = await mbApi.lookup('release', mbRelease.id, ['recordings', 'artist-credits', 'release-groups']); @@ -150,7 +187,7 @@ function App() { artistName: fullMbRelease['artist-credit']?.[0].name || '', // TODO releaseDate: fullMbRelease.date || '', tracks, - coverArtLink: await getCoverArtSrcURL(fullMbRelease.id, fullMbRelease['release-group'].id) || '', + coverArtLink: await getCoverArtSrcURL(fullMbRelease.id, fullMbRelease['release-group']?.id || '') || '', }) console.log(album); setSearching(false); @@ -160,13 +197,30 @@ function App() { if (album) { // If the Album is a PlayableAlbum const player = album.tracks[0]?.source ? ( - + ) : (

This album cannot be played, as Chandler doesn't own a copy.

); return (
- setAlbum(null)} play={play} nowPlaying={nowPlaying} /> +
diff --git a/src/album.tsx b/src/album.tsx index 27754de..7cf5e8c 100644 --- a/src/album.tsx +++ b/src/album.tsx @@ -1,8 +1,8 @@ -export interface Album { +export interface Album { title: string; artistName: string; releaseDate: any; // TODO: Date; - tracks: TTrack[]; + tracks: Track[]; coverArtLink: string; } diff --git a/src/components/Album/Album.tsx b/src/components/Album/Album.tsx index 4c9c1eb..2b84aed 100644 --- a/src/components/Album/Album.tsx +++ b/src/components/Album/Album.tsx @@ -2,7 +2,7 @@ import Track from "./Track"; import "./Album.css"; import type { Album, Track as TrackType } from "../../album"; -export default function Album({ album, dismissAlbum, play, nowPlaying }: { album: Album; dismissAlbum: () => void, play: (track: TrackType) => void, nowPlaying: any }) { +export default function Album({ album, dismissAlbum, play, nowPlaying }: { album: Album; dismissAlbum: () => void, play: (i: number) => void, nowPlaying: TrackType }) { return (
@@ -14,8 +14,8 @@ export default function Album({ album, dismissAlbum, play, nowPlaying }: { album {album.artistName} {album.releaseDate.year ? ' • ' + album.releaseDate.year : ''} {/* TODO: .toLocaleDateString("en-US", { month: "long", day: "numeric", year: "numeric" }) */}

- {album.tracks.map(t => ( - + {album.tracks.map((t, i) => ( + ))}
diff --git a/src/components/Album/Track.tsx b/src/components/Album/Track.tsx index a838f61..aa4f126 100644 --- a/src/components/Album/Track.tsx +++ b/src/components/Album/Track.tsx @@ -1,8 +1,18 @@ import type { Track } from "../../album"; import css from "./Track.module.css"; -export default function Track({track, play, nowPlaying}:{track:any, play: (track:Track) => void, nowPlaying:any}) { - const handler = track.source ? () => play(track) : undefined; +export default function Track({ + track, + idx, + play, + nowPlaying, +}: { + track: Track; + idx: number; + play: (i: number) => void; + nowPlaying: Track | null; +}) { + const handler = track.source ? () => play(idx) : undefined; return ( <>
diff --git a/src/components/NowPlaying/NowPlaying.tsx b/src/components/NowPlaying/NowPlaying.tsx index be88126..55d1ec6 100644 --- a/src/components/NowPlaying/NowPlaying.tsx +++ b/src/components/NowPlaying/NowPlaying.tsx @@ -1,17 +1,39 @@ import type { Track } from '../../album'; import './NowPlaying.css'; -export default function NowPlaying({ nowPlaying, paused, setPaused }: { nowPlaying: Track|null; paused: boolean; setPaused: (paused: boolean) => void }) { +const formatTime = (value: number) => { + const minutes = Math.floor(value / 60); + const seconds = Math.round(value) % 60; + return `${minutes}:${seconds.toString().padStart(2, "0")}`; +}; + +export default function NowPlaying({ + currentTrack, + paused, + currentTime, + duration, + setPaused, +}: { + currentTrack: Track; + paused: boolean; + currentTime: number; + duration: number; + setPaused: (p: boolean) => void; +}) { return (
- +
- {nowPlaying?.title??"—"} - 0:00 / 0:00 + {currentTrack?.title??"—"} + {formatTime(currentTime)} / {formatTime(duration)}
-
+
0 ? Math.min(1, Math.max(0, currentTime / duration)) : 0) * 100}%` }} + >