diff --git a/astro.config.mjs b/astro.config.mjs index bad7ec5..bc27f43 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -19,5 +19,5 @@ export default defineConfig({ '/contact.html': '/contact' }, - integrations: [mdx(), lilypond(), preact()] + integrations: [mdx(), lilypond(), preact({ compat: true })] }); \ No newline at end of file diff --git a/src/components/preact/LBNowPlaying.tsx b/src/components/preact/LBNowPlaying.tsx new file mode 100644 index 0000000..63be85c --- /dev/null +++ b/src/components/preact/LBNowPlaying.tsx @@ -0,0 +1,157 @@ +import { Fragment } from "preact" +import { useEffect, useState } from "preact/hooks" +import "../../styles/components/now_playing.scss" + +interface LBTrack { + track_name: string + release_name?: string + artist_name: string +} + +interface MBArtistCredit { + joinphrase?: string + name: string + artist: { id: string } +} + +interface MBReleaseGroup { + id: string + title: string +} + +interface MBRecording { + id: string + title: string + "artist-credit": MBArtistCredit[] + releases?: { "release-group"?: MBReleaseGroup }[] +} + +interface Song { + title: string + url?: string + artists: ArtistCredit[] + release?: { title: string; url?: string } +} + +interface ArtistCredit { + joinphrase?: string + name: string + url?: string +} + +const makeLBURL = (slug: string) => `https://listenbrainz.org${slug}` + +async function getJSON(url: string): Promise { + try { + const res = await fetch(url) + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`) + return (await res.json()) as T + } catch { + return undefined + } +} + +export default function LBNowPlaying() { + const [song, setSong] = useState() + + useEffect(() => { + const fetchData = async () => { + const rawData = await fetchListenData() + if (!rawData) return + const rid = await labsLookup(rawData.track_name, rawData.artist_name) + const trackData = rid ? await mbLookup(rid) : undefined + if (!trackData) { + setSong(lbToSong(rawData)) + } else { + setSong(mbToSong(trackData)) + } + } + fetchData() + }, []) + + if (!song) return null + + const content = + NOW LISTENING TO −{" "} + {song.title}{" "} + by + {song.release && ( + <> + {" "}on {song.release.title} + + )} [↗] {" • "} + + + return (
+
+ {[content, content, content, content, content]} +
+
) +} + + +function MaybeLink({ href, children }: { href?: string; children: any }) { + return href ? {children} : <>{children} +} + + +function ArtistCredits({ artists }: { artists: ArtistCredit[] }) { + return ( + <> + {artists.map((artist, i) => ( + + {artist.name} + {artist.joinphrase} + + ))} + + ) +} + +async function fetchListenData(): Promise { + const res = await getJSON( + `https://api.listenbrainz.org/1/user/angelcube/playing-now` + ) + return res?.payload?.listens?.[0]?.track_metadata +} + +async function labsLookup(name: string, artist: string) { + const params = new URLSearchParams({ query: `${name} - ${artist}` }) + const res = await getJSON( + `https://labs.api.listenbrainz.org/recording-search/json?${params}` + ) + return res?.[0]?.recording_mbid +} + +async function mbLookup(rid: string) { + const params = new URLSearchParams({ fmt: "json", limit: "1", query: `rid:${rid}` }) + const res = await getJSON<{ recordings?: MBRecording[] }>( + `https://musicbrainz.org/ws/2/recording/?${params}` + ) + return res?.recordings?.[0] +} + +function mbToSong(mb: MBRecording): Song { + const rg = mb.releases?.[0]["release-group"] + const artists = mb["artist-credit"].map(artistCredit => { + return { + joinphrase: artistCredit.joinphrase, + name: artistCredit.name, + url: makeLBURL(`/artist/${artistCredit.artist.id}`) + } + }) + return { + title: mb.title, + url: makeLBURL(`/track/${mb.id}`), + artists, + release: rg ? { title: rg.title, url: makeLBURL(`/album/${rg.id}`)} : undefined + } +} + +function lbToSong(lb: LBTrack): Song { + return { + title: lb.track_name, + artists: [{ name: lb.artist_name }], + release: lb.release_name ? { title: lb.release_name } : undefined + } +} \ No newline at end of file diff --git a/src/pages/index.astro b/src/pages/index.astro index aa104c8..0e1daeb 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,4 +1,5 @@ --- +import LBNowPlaying from "../components/preact/LBNowPlaying"; import "../styles/index.scss" --- @@ -15,6 +16,7 @@ import "../styles/index.scss" +
diff --git a/src/styles/components/now_playing.scss b/src/styles/components/now_playing.scss new file mode 100644 index 0000000..b5d1c75 --- /dev/null +++ b/src/styles/components/now_playing.scss @@ -0,0 +1,47 @@ +div#now-playing-wrapper { + background-color: var(--text-color); + color: var(--background-color); + font-family: var(--font-code); + padding: 0.4em; + line-height: 1.2; + overflow: hidden; + white-space: nowrap; + width: 100%; + + div#now-playing { + animation: marquee 100s linear infinite; + min-width: 100%; + + span { + white-space: nowrap; + a { + color: var(--background-color); + } + } + + &:hover { + animation-play-state: paused; + } + } +} + +@keyframes marquee { + from { + transform: translateX(0); + } + to { + transform: translateX(calc(-100%)); + } +} + +@media (max-width: 775px) { + div#now-playing-wrapper div#now-playing { + animation: marquee 60s linear infinite; + } +} + +@media (max-width: 575px) { + div#now-playing-wrapper div#now-playing { + animation: marquee 20s linear infinite; + } +} \ No newline at end of file