Personal Site
1import { sdk } from "/components/home/playing/spotify";
2import type { nowPlaying } from "/components/home/playing/spotify/client";
3
4export const prerender = false;
5
6export async function GET() {
7 const update = async (): Promise<string> => {
8 // extract a subset to reduce size for client
9 // not huge savings but tesco yk
10 // + reduces chance of leaking extra data to client
11 const playing = await sdk.player.getCurrentlyPlayingTrack().then(
12 (playing) =>
13 // minimise body to make faster and streamline download
14 (!!playing && "album" in playing.item
15 ? {
16 id: playing.item.id,
17 name: playing.item.name,
18 href: playing.item.external_urls.spotify,
19 album: playing.item.album.name,
20 art: playing.item.album.images[0].url,
21 artists: playing.item.artists.map((artist) => ({
22 name: artist.name,
23 href: artist.external_urls.spotify,
24 })),
25 }
26 : null) satisfies nowPlaying,
27 );
28
29 // SSE syntax: https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format
30 return `event: playing\ndata: ${JSON.stringify(playing)}\n\n`;
31 };
32
33 const abort = new AbortController();
34
35 return new Response(
36 new ReadableStream({
37 async start(controller) {
38 while (!abort.signal.aborted) {
39 // dont block loop if spotify API is slow/etc
40 update()
41 // dont write if aborted as it can cause errors
42 .then((val) =>
43 !abort.signal.aborted ? controller.enqueue(val) : undefined,
44 )
45 .catch((err) => {
46 console.error("/now-playing-sse", "GOT ERROR:", err);
47 });
48 await new Promise((res, rej) => {
49 setTimeout(res, 20 * 1000);
50 abort.signal.addEventListener("abort", rej);
51 });
52 }
53 },
54
55 async cancel() {
56 abort.abort();
57 },
58 }),
59 {
60 // Set the headers for Server-Sent Events (SSE)
61 headers: {
62 Connection: "keep-alive",
63 "Content-Encoding": "none",
64 "Cache-Control": "no-cache, no-transform",
65 "Content-Type": "text/event-stream; charset=utf-8",
66 },
67 },
68 );
69}