refactor: simplify LastFM stream server using extracted utilities
- Reduce from 625 lines to 115 lines (82% reduction) - Use LastfmStreamManager to handle complex logic - Cleaner separation of SSE streaming from business logic - Improved error handling and state management This completes the LastFM stream server simplification task. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
a8feb173cb
commit
32fd4b5179
1 changed files with 26 additions and 536 deletions
|
|
@ -1,40 +1,19 @@
|
||||||
import { LastClient } from '@musicorum/lastfm'
|
import { LastClient } from '@musicorum/lastfm'
|
||||||
import type { RequestHandler } from './$types'
|
import type { RequestHandler } from './$types'
|
||||||
import type { Album, AlbumImages } from '$lib/types/lastfm'
|
import { LastfmStreamManager } from '$lib/utils/lastfmStreamManager'
|
||||||
import type { LastfmImage } from '@musicorum/lastfm/dist/types/packages/common'
|
|
||||||
import { findAlbum, transformAlbumData } from '$lib/server/apple-music-client'
|
|
||||||
import redis from '../../redis-client'
|
|
||||||
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
|
||||||
const USERNAME = 'jedmund'
|
const USERNAME = 'jedmund'
|
||||||
const UPDATE_INTERVAL = 30000 // 30 seconds to reduce API load
|
const UPDATE_INTERVAL = 30000 // 30 seconds to reduce API load
|
||||||
|
|
||||||
interface NowPlayingUpdate {
|
|
||||||
albumName: string
|
|
||||||
artistName: string
|
|
||||||
isNowPlaying: boolean
|
|
||||||
nowPlayingTrack?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Store recent tracks for duration-based detection
|
|
||||||
interface TrackPlayInfo {
|
|
||||||
albumName: string
|
|
||||||
trackName: string
|
|
||||||
scrobbleTime: Date
|
|
||||||
durationMs?: number
|
|
||||||
}
|
|
||||||
|
|
||||||
let recentTracks: TrackPlayInfo[] = []
|
|
||||||
|
|
||||||
export const GET: RequestHandler = async ({ request }) => {
|
export const GET: RequestHandler = async ({ request }) => {
|
||||||
const encoder = new TextEncoder()
|
const encoder = new TextEncoder()
|
||||||
|
|
||||||
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 || '')
|
||||||
let lastNowPlayingState: Map<string, { isPlaying: boolean; track?: string }> = new Map()
|
const streamManager = new LastfmStreamManager(client, USERNAME)
|
||||||
let lastAlbumOrder: string[] = [] // Track album order changes
|
|
||||||
let intervalId: NodeJS.Timeout | null = null
|
let intervalId: NodeJS.Timeout | null = null
|
||||||
let isClosed = false
|
let isClosed = false
|
||||||
|
|
||||||
|
|
@ -55,174 +34,38 @@ export const GET: RequestHandler = async ({ request }) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch full album data
|
const update = await streamManager.checkForUpdates()
|
||||||
const albums = await getRecentAlbums(client)
|
|
||||||
|
|
||||||
// Update recentTracks for duration-based now playing detection
|
// Send album updates if any
|
||||||
await getNowPlayingAlbums(client) // This populates recentTracks
|
if (update.albums && !isClosed) {
|
||||||
|
try {
|
||||||
// Enrich albums with additional info and check now playing status
|
const data = JSON.stringify(update.albums)
|
||||||
const enrichedAlbums = await Promise.all(
|
controller.enqueue(encoder.encode(`event: albums\ndata: ${data}\n\n`))
|
||||||
albums.map(async (album) => {
|
|
||||||
try {
|
const nowPlayingAlbum = update.albums.find(a => a.isNowPlaying)
|
||||||
const enriched = await enrichAlbumWithInfo(client, album)
|
logger.music('debug', 'Sent album update with now playing status:', {
|
||||||
const withAppleMusic = await searchAppleMusicForAlbum(enriched)
|
totalAlbums: update.albums.length,
|
||||||
|
nowPlayingAlbum: nowPlayingAlbum
|
||||||
// Check if this album is currently playing using duration-based detection
|
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
|
||||||
if (withAppleMusic.appleMusicData?.tracks && !withAppleMusic.isNowPlaying) {
|
: 'none'
|
||||||
const nowPlayingCheck = checkWithTracks(
|
|
||||||
withAppleMusic.name,
|
|
||||||
withAppleMusic.appleMusicData.tracks
|
|
||||||
)
|
|
||||||
if (nowPlayingCheck?.isNowPlaying) {
|
|
||||||
withAppleMusic.isNowPlaying = true
|
|
||||||
withAppleMusic.nowPlayingTrack = nowPlayingCheck.nowPlayingTrack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return withAppleMusic
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Error enriching album ${album.name}:`,
|
|
||||||
error as Error,
|
|
||||||
undefined,
|
|
||||||
'music'
|
|
||||||
)
|
|
||||||
return album
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// Ensure only one album is marked as now playing in the enriched albums
|
|
||||||
const nowPlayingCount = enrichedAlbums.filter((a) => a.isNowPlaying).length
|
|
||||||
if (nowPlayingCount > 1) {
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Multiple enriched albums marked as now playing (${nowPlayingCount}), keeping only the most recent one`
|
|
||||||
)
|
|
||||||
|
|
||||||
// The albums are already in order from most recent to oldest
|
|
||||||
// So we keep the first now playing album and mark others as not playing
|
|
||||||
let foundFirst = false
|
|
||||||
enrichedAlbums.forEach((album, index) => {
|
|
||||||
if (album.isNowPlaying) {
|
|
||||||
if (foundFirst) {
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Marking album "${album.name}" at position ${index} as not playing`
|
|
||||||
)
|
|
||||||
album.isNowPlaying = false
|
|
||||||
album.nowPlayingTrack = undefined
|
|
||||||
} else {
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Keeping album "${album.name}" at position ${index} as now playing`
|
|
||||||
)
|
|
||||||
foundFirst = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if album order has changed or now playing status changed
|
|
||||||
const currentAlbumOrder = enrichedAlbums.map((a) => `${a.artist.name}:${a.name}`)
|
|
||||||
const albumOrderChanged =
|
|
||||||
JSON.stringify(currentAlbumOrder) !== JSON.stringify(lastAlbumOrder)
|
|
||||||
|
|
||||||
// Also check if any now playing status changed
|
|
||||||
let nowPlayingChanged = false
|
|
||||||
for (const album of enrichedAlbums) {
|
|
||||||
const key = `${album.artist.name}:${album.name}`
|
|
||||||
const lastState = lastNowPlayingState.get(key)
|
|
||||||
if (
|
|
||||||
album.isNowPlaying !== (lastState?.isPlaying || false) ||
|
|
||||||
(album.isNowPlaying && album.nowPlayingTrack !== lastState?.track)
|
|
||||||
) {
|
|
||||||
nowPlayingChanged = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (albumOrderChanged || nowPlayingChanged) {
|
|
||||||
lastAlbumOrder = currentAlbumOrder
|
|
||||||
|
|
||||||
// Update now playing state
|
|
||||||
for (const album of enrichedAlbums) {
|
|
||||||
const key = `${album.artist.name}:${album.name}`
|
|
||||||
lastNowPlayingState.set(key, {
|
|
||||||
isPlaying: album.isNowPlaying || false,
|
|
||||||
track: album.nowPlayingTrack
|
|
||||||
})
|
})
|
||||||
}
|
} catch (e) {
|
||||||
|
isClosed = true
|
||||||
// Send full album update
|
|
||||||
if (!isClosed) {
|
|
||||||
try {
|
|
||||||
const data = JSON.stringify(enrichedAlbums)
|
|
||||||
controller.enqueue(encoder.encode(`event: albums\ndata: ${data}\n\n`))
|
|
||||||
const nowPlayingAlbum = enrichedAlbums.find((a) => a.isNowPlaying)
|
|
||||||
logger.music('debug', 'Sent album update with now playing status:', {
|
|
||||||
totalAlbums: enrichedAlbums.length,
|
|
||||||
nowPlayingAlbum: nowPlayingAlbum
|
|
||||||
? `${nowPlayingAlbum.artist.name} - ${nowPlayingAlbum.name}`
|
|
||||||
: 'none'
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
isClosed = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send now playing updates for albums not in the recent list
|
// Send now playing updates if any
|
||||||
const nowPlayingAlbums = await getNowPlayingAlbums(client)
|
if (update.nowPlayingUpdates && update.nowPlayingUpdates.length > 0 && !isClosed) {
|
||||||
const updates: NowPlayingUpdate[] = []
|
try {
|
||||||
|
const data = JSON.stringify(update.nowPlayingUpdates)
|
||||||
// Only send now playing updates for albums that aren't in the recent albums list
|
controller.enqueue(encoder.encode(`event: nowplaying\ndata: ${data}\n\n`))
|
||||||
// (Recent albums already have their now playing status included)
|
} catch (e) {
|
||||||
for (const album of nowPlayingAlbums) {
|
isClosed = true
|
||||||
const isInRecentAlbums = enrichedAlbums.some(
|
|
||||||
(a) => a.artist.name === album.artistName && a.name === album.albumName
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!isInRecentAlbums) {
|
|
||||||
const key = `${album.artistName}:${album.albumName}`
|
|
||||||
const lastState = lastNowPlayingState.get(key)
|
|
||||||
const wasPlaying = lastState?.isPlaying || false
|
|
||||||
const lastTrack = lastState?.track
|
|
||||||
|
|
||||||
// Update if playing status changed OR if the track changed
|
|
||||||
if (
|
|
||||||
album.isNowPlaying !== wasPlaying ||
|
|
||||||
(album.isNowPlaying && album.nowPlayingTrack !== lastTrack)
|
|
||||||
) {
|
|
||||||
updates.push(album)
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Now playing update for non-recent album ${album.albumName}: playing=${album.isNowPlaying}, track=${album.nowPlayingTrack}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
lastNowPlayingState.set(key, {
|
|
||||||
isPlaying: album.isNowPlaying,
|
|
||||||
track: album.nowPlayingTrack
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if controller is still open before sending
|
// Send heartbeat to keep connection alive
|
||||||
if (!isClosed) {
|
if (!isClosed) {
|
||||||
// Send updates if any
|
|
||||||
if (updates.length > 0) {
|
|
||||||
const data = JSON.stringify(updates)
|
|
||||||
try {
|
|
||||||
controller.enqueue(encoder.encode(`event: nowplaying\ndata: ${data}\n\n`))
|
|
||||||
} catch (e) {
|
|
||||||
// This is expected when client disconnects
|
|
||||||
isClosed = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send heartbeat to keep connection alive
|
|
||||||
try {
|
try {
|
||||||
controller.enqueue(encoder.encode('event: heartbeat\ndata: {}\n\n'))
|
controller.enqueue(encoder.encode('event: heartbeat\ndata: {}\n\n'))
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
@ -269,357 +112,4 @@ export const GET: RequestHandler = async ({ request }) => {
|
||||||
'X-Accel-Buffering': 'no' // Disable Nginx buffering
|
'X-Accel-Buffering': 'no' // Disable Nginx buffering
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getNowPlayingAlbums(client: LastClient): Promise<NowPlayingUpdate[]> {
|
|
||||||
// Check cache for recent tracks
|
|
||||||
const cacheKey = `lastfm:recent:${USERNAME}`
|
|
||||||
const cached = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
let recentTracksResponse
|
|
||||||
if (cached) {
|
|
||||||
logger.music('debug', 'Using cached Last.fm recent tracks for streaming')
|
|
||||||
recentTracksResponse = JSON.parse(cached)
|
|
||||||
// Convert date strings back to Date objects
|
|
||||||
if (recentTracksResponse.tracks) {
|
|
||||||
recentTracksResponse.tracks = recentTracksResponse.tracks.map((track: any) => ({
|
|
||||||
...track,
|
|
||||||
date: track.date ? new Date(track.date) : undefined
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.music('debug', 'Fetching fresh Last.fm recent tracks for streaming')
|
|
||||||
recentTracksResponse = await client.user.getRecentTracks(USERNAME, {
|
|
||||||
limit: 50,
|
|
||||||
extended: true
|
|
||||||
})
|
|
||||||
// Cache for 30 seconds - reasonable for "recent" data
|
|
||||||
await redis.set(cacheKey, JSON.stringify(recentTracksResponse), 'EX', 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
const albums: Map<string, NowPlayingUpdate> = new Map()
|
|
||||||
let hasOfficialNowPlaying = false
|
|
||||||
|
|
||||||
// Clear old tracks and collect new track play information
|
|
||||||
recentTracks = []
|
|
||||||
|
|
||||||
// First pass: check if Last.fm reports any track as now playing
|
|
||||||
for (const track of recentTracksResponse.tracks) {
|
|
||||||
if (track.nowPlaying) {
|
|
||||||
hasOfficialNowPlaying = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const track of recentTracksResponse.tracks) {
|
|
||||||
// Store track play information
|
|
||||||
if (track.date) {
|
|
||||||
recentTracks.push({
|
|
||||||
albumName: track.album.name,
|
|
||||||
trackName: track.name,
|
|
||||||
scrobbleTime: track.date
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const albumKey = `${track.artist.name}:${track.album.name}`
|
|
||||||
|
|
||||||
if (!albums.has(albumKey)) {
|
|
||||||
const album: NowPlayingUpdate = {
|
|
||||||
albumName: track.album.name,
|
|
||||||
artistName: track.artist.name,
|
|
||||||
isNowPlaying: track.nowPlaying || false,
|
|
||||||
nowPlayingTrack: track.nowPlaying ? track.name : undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only use duration-based detection if Last.fm doesn't report any now playing
|
|
||||||
if (!album.isNowPlaying && !hasOfficialNowPlaying) {
|
|
||||||
const updatedStatus = await checkNowPlayingWithDuration(album.albumName, album.artistName)
|
|
||||||
if (updatedStatus) {
|
|
||||||
album.isNowPlaying = updatedStatus.isNowPlaying
|
|
||||||
album.nowPlayingTrack = updatedStatus.nowPlayingTrack
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
albums.set(albumKey, album)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure only one album is marked as now playing - keep the most recent one
|
|
||||||
const nowPlayingAlbums = Array.from(albums.values()).filter((a) => a.isNowPlaying)
|
|
||||||
if (nowPlayingAlbums.length > 1) {
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Multiple albums marked as now playing (${nowPlayingAlbums.length}), keeping only the most recent one`
|
|
||||||
)
|
|
||||||
|
|
||||||
// Find the most recent track
|
|
||||||
let mostRecentTime = new Date(0)
|
|
||||||
let mostRecentAlbum = nowPlayingAlbums[0]
|
|
||||||
|
|
||||||
for (const album of nowPlayingAlbums) {
|
|
||||||
// Find the most recent track for this album
|
|
||||||
const albumTracks = recentTracks.filter((t) => t.albumName === album.albumName)
|
|
||||||
if (albumTracks.length > 0) {
|
|
||||||
const latestTrack = albumTracks.reduce((latest, track) =>
|
|
||||||
track.scrobbleTime > latest.scrobbleTime ? track : latest
|
|
||||||
)
|
|
||||||
if (latestTrack.scrobbleTime > mostRecentTime) {
|
|
||||||
mostRecentTime = latestTrack.scrobbleTime
|
|
||||||
mostRecentAlbum = album
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Mark all others as not playing
|
|
||||||
nowPlayingAlbums.forEach((album) => {
|
|
||||||
if (album !== mostRecentAlbum) {
|
|
||||||
const key = `${album.artistName}:${album.albumName}`
|
|
||||||
albums.set(key, {
|
|
||||||
...album,
|
|
||||||
isNowPlaying: false,
|
|
||||||
nowPlayingTrack: undefined
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(albums.values())
|
|
||||||
}
|
|
||||||
|
|
||||||
async function checkNowPlayingWithDuration(
|
|
||||||
albumName: string,
|
|
||||||
artistName: string
|
|
||||||
): Promise<{ isNowPlaying: boolean; nowPlayingTrack?: string } | null> {
|
|
||||||
try {
|
|
||||||
// Check cache for Apple Music data
|
|
||||||
const cacheKey = `apple:album:${artistName}:${albumName}`
|
|
||||||
const cached = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
if (!cached) {
|
|
||||||
// Try to fetch from Apple Music if not cached
|
|
||||||
const appleMusicAlbum = await findAlbum(artistName, albumName)
|
|
||||||
if (!appleMusicAlbum) return null
|
|
||||||
|
|
||||||
const transformedData = await transformAlbumData(appleMusicAlbum)
|
|
||||||
await redis.set(cacheKey, JSON.stringify(transformedData), 'EX', 86400)
|
|
||||||
return checkWithTracks(albumName, transformedData.tracks)
|
|
||||||
}
|
|
||||||
|
|
||||||
const appleMusicData = JSON.parse(cached)
|
|
||||||
return checkWithTracks(albumName, appleMusicData.tracks)
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(`Error checking duration for ${albumName}:`, error as Error, undefined, 'music')
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function checkWithTracks(
|
|
||||||
albumName: string,
|
|
||||||
tracks?: Array<{ name: string; durationMs?: number }>
|
|
||||||
): { isNowPlaying: boolean; nowPlayingTrack?: string } | null {
|
|
||||||
if (!tracks) return null
|
|
||||||
|
|
||||||
const now = new Date()
|
|
||||||
const SCROBBLE_LAG = 3 * 60 * 1000 // 3 minutes
|
|
||||||
|
|
||||||
// Clean up old tracks first
|
|
||||||
recentTracks = recentTracks.filter((track) => {
|
|
||||||
// Keep tracks from last hour only
|
|
||||||
const hourAgo = new Date(now.getTime() - 60 * 60 * 1000)
|
|
||||||
return track.scrobbleTime > hourAgo
|
|
||||||
})
|
|
||||||
|
|
||||||
// Find the most recent track from this album
|
|
||||||
let mostRecentTrack: TrackPlayInfo | null = null
|
|
||||||
for (const trackInfo of recentTracks) {
|
|
||||||
if (trackInfo.albumName === albumName) {
|
|
||||||
if (!mostRecentTrack || trackInfo.scrobbleTime > mostRecentTrack.scrobbleTime) {
|
|
||||||
mostRecentTrack = trackInfo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mostRecentTrack) {
|
|
||||||
const trackData = tracks.find(
|
|
||||||
(t) => t.name.toLowerCase() === mostRecentTrack.trackName.toLowerCase()
|
|
||||||
)
|
|
||||||
|
|
||||||
if (trackData?.durationMs) {
|
|
||||||
const trackEndTime = new Date(
|
|
||||||
mostRecentTrack.scrobbleTime.getTime() + trackData.durationMs + SCROBBLE_LAG
|
|
||||||
)
|
|
||||||
|
|
||||||
if (now < trackEndTime) {
|
|
||||||
logger.music(
|
|
||||||
'debug',
|
|
||||||
`Track "${mostRecentTrack.trackName}" is still playing (ends at ${trackEndTime.toLocaleTimeString()})`
|
|
||||||
)
|
|
||||||
return {
|
|
||||||
isNowPlaying: true,
|
|
||||||
nowPlayingTrack: mostRecentTrack.trackName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return { isNowPlaying: false }
|
|
||||||
}
|
|
||||||
|
|
||||||
// Helper functions for album data
|
|
||||||
function transformImages(images: LastfmImage[]): AlbumImages {
|
|
||||||
const imageMap: AlbumImages = {
|
|
||||||
small: '',
|
|
||||||
medium: '',
|
|
||||||
large: '',
|
|
||||||
extralarge: '',
|
|
||||||
mega: '',
|
|
||||||
default: ''
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const image of images) {
|
|
||||||
const size = image.size as keyof AlbumImages
|
|
||||||
if (size in imageMap) {
|
|
||||||
imageMap[size] = image.url
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return imageMap
|
|
||||||
}
|
|
||||||
|
|
||||||
async function enrichAlbumWithInfo(client: LastClient, album: Album): Promise<Album> {
|
|
||||||
// Check cache for album info
|
|
||||||
const cacheKey = `lastfm:albuminfo:${album.artist.name}:${album.name}`
|
|
||||||
const cached = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
logger.music('debug', `Using cached album info for "${album.name}"`)
|
|
||||||
const albumInfo = JSON.parse(cached)
|
|
||||||
return {
|
|
||||||
...album,
|
|
||||||
url: albumInfo?.url || '',
|
|
||||||
images: transformImages(albumInfo?.images || [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.music('debug', `Fetching fresh album info for "${album.name}"`)
|
|
||||||
const albumInfo = await client.album.getInfo(album.name, album.artist.name)
|
|
||||||
|
|
||||||
// Cache for 1 hour - album info rarely changes
|
|
||||||
await redis.set(cacheKey, JSON.stringify(albumInfo), 'EX', 3600)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...album,
|
|
||||||
url: albumInfo?.url || '',
|
|
||||||
images: transformImages(albumInfo?.images || [])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchAppleMusicForAlbum(album: Album): Promise<Album> {
|
|
||||||
try {
|
|
||||||
// Check cache first
|
|
||||||
const cacheKey = `apple:album:${album.artist.name}:${album.name}`
|
|
||||||
const cached = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
if (cached) {
|
|
||||||
const cachedData = JSON.parse(cached)
|
|
||||||
return {
|
|
||||||
...album,
|
|
||||||
images: {
|
|
||||||
...album.images,
|
|
||||||
itunes: cachedData.highResArtwork || album.images.itunes
|
|
||||||
},
|
|
||||||
appleMusicData: cachedData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Search Apple Music
|
|
||||||
const appleMusicAlbum = await findAlbum(album.artist.name, album.name)
|
|
||||||
|
|
||||||
if (appleMusicAlbum) {
|
|
||||||
const transformedData = await transformAlbumData(appleMusicAlbum)
|
|
||||||
|
|
||||||
// Cache the result for 24 hours
|
|
||||||
await redis.set(cacheKey, JSON.stringify(transformedData), 'EX', 86400)
|
|
||||||
|
|
||||||
return {
|
|
||||||
...album,
|
|
||||||
images: {
|
|
||||||
...album.images,
|
|
||||||
itunes: transformedData.highResArtwork || album.images.itunes
|
|
||||||
},
|
|
||||||
appleMusicData: transformedData
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
logger.error(
|
|
||||||
`Failed to fetch Apple Music data for "${album.name}" by "${album.artist.name}":`,
|
|
||||||
error as Error,
|
|
||||||
undefined,
|
|
||||||
'music'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return album unchanged if Apple Music search fails
|
|
||||||
return album
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRecentAlbums(client: LastClient, limit: number = 4): Promise<Album[]> {
|
|
||||||
// Check cache for recent tracks
|
|
||||||
const cacheKey = `lastfm:recent:${USERNAME}`
|
|
||||||
const cached = await redis.get(cacheKey)
|
|
||||||
|
|
||||||
let recentTracksResponse
|
|
||||||
if (cached) {
|
|
||||||
logger.music('debug', 'Using cached Last.fm recent tracks for album stream')
|
|
||||||
recentTracksResponse = JSON.parse(cached)
|
|
||||||
// Convert date strings back to Date objects
|
|
||||||
if (recentTracksResponse.tracks) {
|
|
||||||
recentTracksResponse.tracks = recentTracksResponse.tracks.map((track: any) => ({
|
|
||||||
...track,
|
|
||||||
date: track.date ? new Date(track.date) : undefined
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
logger.music('debug', 'Fetching fresh Last.fm recent tracks for album stream')
|
|
||||||
recentTracksResponse = await client.user.getRecentTracks(USERNAME, {
|
|
||||||
limit: 50,
|
|
||||||
extended: true
|
|
||||||
})
|
|
||||||
// Cache for 30 seconds - reasonable for "recent" data
|
|
||||||
await redis.set(cacheKey, JSON.stringify(recentTracksResponse), 'EX', 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
const uniqueAlbums = new Map<string, Album>()
|
|
||||||
|
|
||||||
for (const track of recentTracksResponse.tracks) {
|
|
||||||
if (uniqueAlbums.size >= limit) break
|
|
||||||
|
|
||||||
const albumKey = `${track.album.mbid || track.album.name}`
|
|
||||||
if (!uniqueAlbums.has(albumKey)) {
|
|
||||||
uniqueAlbums.set(albumKey, {
|
|
||||||
name: track.album.name,
|
|
||||||
artist: {
|
|
||||||
name: track.artist.name,
|
|
||||||
mbid: track.artist.mbid || ''
|
|
||||||
},
|
|
||||||
playCount: 1,
|
|
||||||
images: transformImages(track.images),
|
|
||||||
mbid: track.album.mbid || '',
|
|
||||||
url: track.url,
|
|
||||||
rank: uniqueAlbums.size + 1,
|
|
||||||
isNowPlaying: track.nowPlaying || false,
|
|
||||||
nowPlayingTrack: track.nowPlaying ? track.name : undefined
|
|
||||||
})
|
|
||||||
} else if (track.nowPlaying) {
|
|
||||||
// If album already exists but this track is now playing, update it
|
|
||||||
const existingAlbum = uniqueAlbums.get(albumKey)!
|
|
||||||
uniqueAlbums.set(albumKey, {
|
|
||||||
...existingAlbum,
|
|
||||||
isNowPlaying: true,
|
|
||||||
nowPlayingTrack: track.name
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Array.from(uniqueAlbums.values())
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue