An API for my personal portfolio
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

Merge pull request #2 from matthew-hre/matthew-hre/jj-pkwwtvswxnlo

authored by

Matthew Hrehirchuk and committed by
GitHub
ed9ec02a c73e9c1d

+128
+2
src/index.ts
··· 1 1 import { Hono } from "hono"; 2 2 import { cors } from "hono/cors"; 3 3 import { vinyl } from "./routes/vinyl"; 4 + import { activity } from "./routes/activity"; 4 5 5 6 const app = new Hono(); 6 7 ··· 16 17 ); 17 18 18 19 app.route("/vinyl", vinyl); 20 + app.route("/activity", activity); 19 21 20 22 app.get("/", (c) => c.json({ status: "ok" })); 21 23
+89
src/lib/lastfm.ts
··· 1 + const LASTFM_API_KEY = process.env.LASTFM_API_KEY!; 2 + const LASTFM_USERNAME = process.env.LASTFM_USERNAME!; 3 + const POLL_INTERVAL = 10_000; 4 + 5 + export interface Track { 6 + name: string; 7 + artist: string; 8 + album: string; 9 + image: string; 10 + url: string; 11 + nowPlaying: boolean; 12 + timestamp: string | null; 13 + } 14 + 15 + type Listener = (track: Track | null) => void; 16 + 17 + let currentTrack: Track | null = null; 18 + const listeners = new Set<Listener>(); 19 + let polling = false; 20 + 21 + function parseTrack(raw: Record<string, unknown>): Track { 22 + const artist = raw.artist as Record<string, string>; 23 + const album = raw.album as Record<string, string>; 24 + const images = raw.image as Array<Record<string, string>>; 25 + const attr = raw["@attr"] as Record<string, string> | undefined; 26 + const date = raw.date as Record<string, string> | undefined; 27 + 28 + return { 29 + name: raw.name as string, 30 + artist: artist["#text"], 31 + album: album["#text"], 32 + image: 33 + images.find((i) => i.size === "large")?.["#text"] || 34 + images.find((i) => i.size === "medium")?.["#text"] || 35 + "", 36 + url: raw.url as string, 37 + nowPlaying: attr?.nowplaying === "true", 38 + timestamp: date?.uts || null, 39 + }; 40 + } 41 + 42 + function trackChanged(a: Track | null, b: Track | null): boolean { 43 + if (a === null || b === null) return a !== b; 44 + return ( 45 + a.name !== b.name || 46 + a.artist !== b.artist || 47 + a.nowPlaying !== b.nowPlaying 48 + ); 49 + } 50 + 51 + async function poll() { 52 + try { 53 + const res = await fetch( 54 + `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${LASTFM_USERNAME}&api_key=${LASTFM_API_KEY}&format=json&limit=1` 55 + ); 56 + 57 + if (!res.ok) return; 58 + 59 + const data = await res.json(); 60 + const rawTrack = data?.recenttracks?.track?.[0]; 61 + const track = rawTrack ? parseTrack(rawTrack) : null; 62 + 63 + if (trackChanged(currentTrack, track)) { 64 + currentTrack = track; 65 + for (const listener of listeners) { 66 + listener(track); 67 + } 68 + } 69 + } catch (err) { 70 + console.error("Last.fm poll failed:", err); 71 + } 72 + } 73 + 74 + function startPolling() { 75 + if (polling) return; 76 + polling = true; 77 + poll(); 78 + setInterval(poll, POLL_INTERVAL); 79 + } 80 + 81 + export function getCurrentTrack(): Track | null { 82 + return currentTrack; 83 + } 84 + 85 + export function subscribe(listener: Listener): () => void { 86 + listeners.add(listener); 87 + startPolling(); 88 + return () => listeners.delete(listener); 89 + }
+37
src/routes/activity.ts
··· 1 + import { Hono } from "hono"; 2 + import { streamSSE } from "hono/streaming"; 3 + import { getCurrentTrack, subscribe } from "../lib/lastfm"; 4 + 5 + const activity = new Hono(); 6 + 7 + activity.get("/music", (c) => { 8 + return c.json({ track: getCurrentTrack() }); 9 + }); 10 + 11 + activity.get("/music/stream", (c) => { 12 + return streamSSE(c, async (stream) => { 13 + const track = getCurrentTrack(); 14 + await stream.writeSSE({ 15 + data: JSON.stringify({ track }), 16 + event: "track", 17 + }); 18 + 19 + const unsubscribe = subscribe((track) => { 20 + stream.writeSSE({ 21 + data: JSON.stringify({ track }), 22 + event: "track", 23 + }); 24 + }); 25 + 26 + stream.onAbort(() => { 27 + unsubscribe(); 28 + }); 29 + 30 + // Keep the connection open 31 + while (true) { 32 + await stream.sleep(30_000); 33 + } 34 + }); 35 + }); 36 + 37 + export { activity };