Implement WebSocket connection for real-time song updates and refactor Last.fm song fetching logic
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s

This commit is contained in:
Cameron Redmore 2025-08-08 12:19:45 +01:00
parent db371a6d21
commit a6dfc77244
No known key found for this signature in database
2 changed files with 225 additions and 112 deletions

View file

@ -1,87 +1,119 @@
import { Elysia, t } from 'elysia';
import { staticPlugin } from '@elysiajs/static';
// Simple cache implementation
interface CacheEntry {
data: any;
timestamp: number;
// Song data interface
interface LastSong {
name: string;
artist: string;
album: string;
url: string;
nowPlaying: boolean;
image: any[];
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL = 30 * 1000; // 30 seconds in milliseconds
// Current song state
let currentSong: LastSong | null = null;
let lastSongId: string | null = null;
function getCachedData(key: string): any | null {
const entry = cache.get(key);
if (!entry) return null;
const now = Date.now();
if (now - entry.timestamp > CACHE_TTL) {
cache.delete(key);
// WebSocket connections
const connections = new Set<any>();
// Function to fetch song from Last.fm
async function fetchLastFmSong(): Promise<LastSong | null> {
const apiKey = process.env.LASTFM_API_KEY;
const username = process.env.LASTFM_USERNAME;
if (!apiKey || !username) {
console.error('Last.fm API key or username not configured');
return null;
}
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`;
try {
const response = await fetch(url);
const data = await response.json();
const track = data.recenttracks.track[0];
return {
name: track.name,
artist: track.artist['#text'],
album: track.album['#text'],
url: track.url,
nowPlaying: track['@attr']?.nowplaying === 'true',
image: track.image || [],
};
} catch (error) {
console.error('Error fetching from Last.fm:', error);
return null;
}
return entry.data;
}
function setCachedData(key: string, data: any): void {
cache.set(key, {
data,
timestamp: Date.now()
});
// Function to check for song changes and notify clients
async function checkAndUpdateSong(): Promise<void> {
const newSong = await fetchLastFmSong();
if (!newSong) return;
// Create a unique identifier for the song
const newSongId = `${newSong.artist}-${newSong.name}-${newSong.nowPlaying}`;
// Check if song has changed
if (newSongId !== lastSongId) {
console.log('Song changed:', newSong.name, 'by', newSong.artist);
currentSong = newSong;
lastSongId = newSongId;
// Notify all connected WebSocket clients
const message = JSON.stringify({
type: 'song-update',
data: newSong
});
connections.forEach(ws => {
try {
ws.send(message);
} catch (error) {
console.error('Error sending to WebSocket client:', error);
connections.delete(ws);
}
});
}
}
// Start polling Last.fm every second
setInterval(checkAndUpdateSong, 1000);
// Initial fetch
checkAndUpdateSong();
const app = new Elysia()
.use(staticPlugin({
assets: 'dist',
prefix: ''
}))
.get('/api/last-song', async () => {
const apiKey = Bun.env.LASTFM_API_KEY;
const username = Bun.env.LASTFM_USERNAME;
if (!apiKey || !username) {
return new Response(
JSON.stringify({ error: 'Last.fm API key or username not configured' }),
{ status: 500, headers: { 'Content-Type': 'application/json' } }
);
}
// Check cache first
const cacheKey = `lastfm:${username}`;
const cachedData = getCachedData(cacheKey);
if (cachedData) {
console.log('Returning cached Last.fm data');
return cachedData;
}
const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${username}&api_key=${apiKey}&format=json&limit=1`;
try {
const response = await fetch(url);
const data = await response.json();
const track = data.recenttracks.track[0];
.ws('/music', {
message(ws, message) {
// Handle incoming messages if needed
console.log('Received message from client:', message);
},
open(ws) {
console.log('WebSocket client connected');
connections.add(ws);
const result = {
name: track.name,
artist: track.artist['#text'],
album: track.album['#text'],
url: track.url,
nowPlaying: track['@attr']?.nowplaying === 'true',
image: track.image || [],
};
// Cache the result
setCachedData(cacheKey, result);
console.log('Fetched and cached new Last.fm data');
return result;
} catch (error) {
console.error('Error fetching from Last.fm:', error);
return new Response(JSON.stringify({ error: 'Failed to fetch last song' }), {
status: 500,
headers: { 'Content-Type': 'application/json' },
});
}
// Send current song to new client immediately
if (currentSong) {
const message = JSON.stringify({
type: 'song-update',
data: currentSong
});
ws.send(message);
}
},
close(ws) {
console.log('WebSocket client disconnected');
connections.delete(ws);
},
})
.listen({
port: process.env.PORT || 3000,