From b84a2637c0aff68f6bd393eaedcdc28b553e1b51 Mon Sep 17 00:00:00 2001 From: Justin Edmund Date: Thu, 10 Jul 2025 21:32:29 -0700 Subject: [PATCH] feat: implement smart polling intervals based on track duration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Adjust update frequency based on remaining track time - Poll every 5s when <20s remaining, 10s for 20-60s, 15s for >60s - Add heartbeat timestamps to track update timing - Implement time-based fallback for tracks without Apple Music data - Assume tracks scrobbled within 5 minutes are still playing 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/lib/utils/lastfmTransformers.ts | 3 +- src/routes/api/lastfm/stream/+server.ts | 120 ++++++++++++++---------- 2 files changed, 75 insertions(+), 48 deletions(-) diff --git a/src/lib/utils/lastfmTransformers.ts b/src/lib/utils/lastfmTransformers.ts index a98fd50..2dc9fd1 100644 --- a/src/lib/utils/lastfmTransformers.ts +++ b/src/lib/utils/lastfmTransformers.ts @@ -56,7 +56,8 @@ export function trackToAlbum(track: any, rank: number): Album { url: track.url, rank, isNowPlaying: track.nowPlaying || false, - nowPlayingTrack: track.nowPlaying ? track.name : undefined + nowPlayingTrack: track.nowPlaying ? track.name : undefined, + lastScrobbleTime: track.date || track.nowPlaying ? new Date() : undefined } } diff --git a/src/routes/api/lastfm/stream/+server.ts b/src/routes/api/lastfm/stream/+server.ts index f0e6bb3..1a75d58 100644 --- a/src/routes/api/lastfm/stream/+server.ts +++ b/src/routes/api/lastfm/stream/+server.ts @@ -1,6 +1,6 @@ import { LastClient } from '@musicorum/lastfm' import type { RequestHandler } from './$types' -import { LastfmStreamManager } from '$lib/utils/lastfmStreamManager' +import { SimpleLastfmStreamManager } from '$lib/utils/simpleLastfmStreamManager' import { logger } from '$lib/server/logger' const LASTFM_API_KEY = process.env.LASTFM_API_KEY @@ -14,7 +14,7 @@ export const GET: RequestHandler = async ({ request }) => { const stream = new ReadableStream({ async start(controller) { const client = new LastClient(LASTFM_API_KEY || '') - const streamManager = new LastfmStreamManager(client, USERNAME) + const streamManager = new SimpleLastfmStreamManager(client, USERNAME) let intervalId: NodeJS.Timeout | null = null let isClosed = false let currentInterval = UPDATE_INTERVAL @@ -39,65 +39,91 @@ export const GET: RequestHandler = async ({ request }) => { try { const update = await streamManager.checkForUpdates() - // Check if music is playing - let musicIsPlaying = false - // Send album updates if any if (update.albums && !isClosed) { try { const data = JSON.stringify(update.albums) controller.enqueue(encoder.encode(`event: albums\ndata: ${data}\n\n`)) + // Check if music is playing and calculate smart interval const nowPlayingAlbum = update.albums.find((a) => a.isNowPlaying) - musicIsPlaying = !!nowPlayingAlbum + const musicIsPlaying = !!nowPlayingAlbum - logger.music('debug', 'Sent album update with now playing status:', { - totalAlbums: update.albums.length, - nowPlayingAlbum: nowPlayingAlbum - ? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}` - : 'none' - }) - } catch (e) { - isClosed = true - } - } - - // Send now playing updates if any - if (update.nowPlayingUpdates && update.nowPlayingUpdates.length > 0 && !isClosed) { - try { - const data = JSON.stringify(update.nowPlayingUpdates) - controller.enqueue(encoder.encode(`event: nowplaying\ndata: ${data}\n\n`)) - - // Check if any of the updates indicate music is playing - musicIsPlaying = musicIsPlaying || update.nowPlayingUpdates.some(u => u.isNowPlaying) - } catch (e) { - isClosed = true - } - } - - // Adjust polling interval based on playing state - if (musicIsPlaying !== isPlaying) { - isPlaying = musicIsPlaying - const newInterval = isPlaying ? FAST_UPDATE_INTERVAL : UPDATE_INTERVAL - - if (newInterval !== currentInterval) { - currentInterval = newInterval - logger.music('debug', `Adjusting polling interval to ${currentInterval}ms (playing: ${isPlaying})`) - - // Reset interval with new timing - if (intervalId) { - clearInterval(intervalId) - intervalId = setInterval(checkForUpdates, currentInterval) + // Calculate remaining time if we have track duration + let remainingMs = 0 + if (nowPlayingAlbum?.nowPlayingTrack && nowPlayingAlbum.appleMusicData?.tracks) { + const track = nowPlayingAlbum.appleMusicData.tracks.find( + t => t.name === nowPlayingAlbum.nowPlayingTrack + ) + + if (track?.durationMs && nowPlayingAlbum.lastScrobbleTime) { + const elapsed = Date.now() - new Date(nowPlayingAlbum.lastScrobbleTime).getTime() + remainingMs = Math.max(0, track.durationMs - elapsed) + } } + + logger.music('debug', '📤 SSE: Sent album update:', { + totalAlbums: update.albums.length, + nowPlaying: nowPlayingAlbum + ? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}` + : 'none', + remainingMs: remainingMs, + albumsWithStatus: update.albums.map(a => ({ + name: a.name, + artist: a.artist.name, + isNowPlaying: a.isNowPlaying, + track: a.nowPlayingTrack + })) + }) + + // Smart interval adjustment based on remaining track time + let targetInterval = UPDATE_INTERVAL // Default 30s + + if (musicIsPlaying && remainingMs > 0) { + // If track is ending soon (within 20 seconds), check more frequently + if (remainingMs < 20000) { + targetInterval = 5000 // 5 seconds + } + // If track has 20-60 seconds left, moderate frequency + else if (remainingMs < 60000) { + targetInterval = 10000 // 10 seconds + } + // If track has more than 60 seconds, check every 15 seconds + else { + targetInterval = 15000 // 15 seconds + } + } else if (musicIsPlaying) { + // If playing but no duration info, use fast interval + targetInterval = FAST_UPDATE_INTERVAL + } + + // Apply new interval if it changed significantly (more than 1 second difference) + if (Math.abs(targetInterval - currentInterval) > 1000) { + currentInterval = targetInterval + logger.music('debug', `Adjusting interval to ${currentInterval}ms (playing: ${isPlaying}, remaining: ${Math.round(remainingMs/1000)}s)`) + + // Reset interval with new timing + if (intervalId) { + clearInterval(intervalId) + intervalId = setInterval(checkForUpdates, currentInterval) + } + } + } catch (e) { + isClosed = true } } - // Send heartbeat to keep connection alive + // Always send heartbeat with timestamp to keep client synced if (!isClosed) { try { - controller.enqueue(encoder.encode('event: heartbeat\ndata: {}\n\n')) + const heartbeatData = JSON.stringify({ + timestamp: new Date().toISOString(), + interval: currentInterval, + hasUpdates: !!update.albums + }) + controller.enqueue(encoder.encode(`event: heartbeat\ndata: ${heartbeatData}\n\n`)) } catch (e) { - // This is expected when client disconnects + // Expected when client disconnects isClosed = true } } @@ -140,4 +166,4 @@ export const GET: RequestHandler = async ({ request }) => { 'X-Accel-Buffering': 'no' // Disable Nginx buffering } }) -} +} \ No newline at end of file