feat: implement smart polling intervals based on track duration
- 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 <noreply@anthropic.com>
This commit is contained in:
parent
3d7eb6e985
commit
b84a2637c0
2 changed files with 75 additions and 48 deletions
|
|
@ -56,7 +56,8 @@ export function trackToAlbum(track: any, rank: number): Album {
|
||||||
url: track.url,
|
url: track.url,
|
||||||
rank,
|
rank,
|
||||||
isNowPlaying: track.nowPlaying || false,
|
isNowPlaying: track.nowPlaying || false,
|
||||||
nowPlayingTrack: track.nowPlaying ? track.name : undefined
|
nowPlayingTrack: track.nowPlaying ? track.name : undefined,
|
||||||
|
lastScrobbleTime: track.date || track.nowPlaying ? new Date() : undefined
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import { LastClient } from '@musicorum/lastfm'
|
import { LastClient } from '@musicorum/lastfm'
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import { LastfmStreamManager } from '$lib/utils/lastfmStreamManager'
|
import { SimpleLastfmStreamManager } from '$lib/utils/simpleLastfmStreamManager'
|
||||||
import { logger } from '$lib/server/logger'
|
import { logger } from '$lib/server/logger'
|
||||||
|
|
||||||
const LASTFM_API_KEY = process.env.LASTFM_API_KEY
|
const LASTFM_API_KEY = process.env.LASTFM_API_KEY
|
||||||
|
|
@ -14,7 +14,7 @@ export const GET: RequestHandler = async ({ request }) => {
|
||||||
const stream = new ReadableStream({
|
const stream = new ReadableStream({
|
||||||
async start(controller) {
|
async start(controller) {
|
||||||
const client = new LastClient(LASTFM_API_KEY || '')
|
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 intervalId: NodeJS.Timeout | null = null
|
||||||
let isClosed = false
|
let isClosed = false
|
||||||
let currentInterval = UPDATE_INTERVAL
|
let currentInterval = UPDATE_INTERVAL
|
||||||
|
|
@ -39,65 +39,91 @@ export const GET: RequestHandler = async ({ request }) => {
|
||||||
try {
|
try {
|
||||||
const update = await streamManager.checkForUpdates()
|
const update = await streamManager.checkForUpdates()
|
||||||
|
|
||||||
// Check if music is playing
|
|
||||||
let musicIsPlaying = false
|
|
||||||
|
|
||||||
// Send album updates if any
|
// Send album updates if any
|
||||||
if (update.albums && !isClosed) {
|
if (update.albums && !isClosed) {
|
||||||
try {
|
try {
|
||||||
const data = JSON.stringify(update.albums)
|
const data = JSON.stringify(update.albums)
|
||||||
controller.enqueue(encoder.encode(`event: albums\ndata: ${data}\n\n`))
|
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)
|
const nowPlayingAlbum = update.albums.find((a) => a.isNowPlaying)
|
||||||
musicIsPlaying = !!nowPlayingAlbum
|
const musicIsPlaying = !!nowPlayingAlbum
|
||||||
|
|
||||||
logger.music('debug', 'Sent album update with now playing status:', {
|
// Calculate remaining time if we have track duration
|
||||||
totalAlbums: update.albums.length,
|
let remainingMs = 0
|
||||||
nowPlayingAlbum: nowPlayingAlbum
|
if (nowPlayingAlbum?.nowPlayingTrack && nowPlayingAlbum.appleMusicData?.tracks) {
|
||||||
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
|
const track = nowPlayingAlbum.appleMusicData.tracks.find(
|
||||||
: 'none'
|
t => t.name === nowPlayingAlbum.nowPlayingTrack
|
||||||
})
|
)
|
||||||
} catch (e) {
|
|
||||||
isClosed = true
|
if (track?.durationMs && nowPlayingAlbum.lastScrobbleTime) {
|
||||||
}
|
const elapsed = Date.now() - new Date(nowPlayingAlbum.lastScrobbleTime).getTime()
|
||||||
}
|
remainingMs = Math.max(0, track.durationMs - elapsed)
|
||||||
|
}
|
||||||
// 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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
if (!isClosed) {
|
||||||
try {
|
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) {
|
} catch (e) {
|
||||||
// This is expected when client disconnects
|
// Expected when client disconnects
|
||||||
isClosed = true
|
isClosed = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -140,4 +166,4 @@ export const GET: RequestHandler = async ({ request }) => {
|
||||||
'X-Accel-Buffering': 'no' // Disable Nginx buffering
|
'X-Accel-Buffering': 'no' // Disable Nginx buffering
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
Loading…
Reference in a new issue