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
All checks were successful
Build and Push Docker Image / build-and-push (push) Successful in 1m17s
This commit is contained in:
parent
db371a6d21
commit
a6dfc77244
2 changed files with 225 additions and 112 deletions
164
src/server.ts
164
src/server.ts
|
@ -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,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue