jedmund-svelte/src/lib/utils/lastfmStreamManager.ts
Justin Edmund 9f2854bfdc fix: replace any types in music integration utilities (19 errors)
Phase 1 Batch 5: Music Integration type safety improvements

Fixed 19 any-type errors across 6 music integration files:

1. src/lib/utils/albumEnricher.ts (4 errors)
   - Created RecentTracksData interface
   - Fixed getAppleMusicDataForNowPlaying: return Album['appleMusicData'] | null
   - Fixed cacheRecentTracks: parameter RecentTracksData
   - Fixed getCachedRecentTracks: return RecentTracksData | null
   - Fixed getCachedRecentTracks: data typing

2. src/lib/utils/lastfmStreamManager.ts (4 errors)
   - Created RecentTracksResponse interface
   - Fixed fetchFreshRecentTracks: return RecentTracksResponse
   - Fixed getRecentAlbums: parameter RecentTracksResponse
   - Fixed updateNowPlayingStatus: parameter RecentTracksResponse
   - Fixed getNowPlayingUpdatesForNonRecentAlbums: parameter RecentTracksResponse

3. src/lib/utils/lastfmTransformers.ts (2 errors)
   - Created LastfmTrack interface
   - Fixed trackToAlbum: parameter LastfmTrack
   - Fixed mergeAppleMusicData: parameter Album['appleMusicData']

4. src/lib/utils/nowPlayingDetector.ts (4 errors)
   - Created LastfmRecentTrack and RecentTracksResponse interfaces
   - Fixed processNowPlayingTracks: parameters with proper types
   - Fixed detectNowPlayingAlbums: parameters with proper types
   - Updated appleMusicDataLookup callback: return Album['appleMusicData'] | null

5. src/lib/utils/simpleNowPlayingDetector.ts (3 errors)
   - Created LastfmTrack interface
   - Fixed processAlbums: recentTracks parameter to LastfmTrack[]
   - Fixed appleMusicDataLookup callback: return Album['appleMusicData'] | null
   - Fixed mostRecentTrack variable type and date handling
   - Fixed trackData type in tracks.find()

6. src/lib/utils/simpleLastfmStreamManager.ts (2 errors)
   - Created RecentTracksResponse interface
   - Fixed getRecentAlbums: parameter RecentTracksResponse

Progress: 32 any-type errors remaining (down from 51)
2025-11-24 02:25:23 -08:00

309 lines
8.8 KiB
TypeScript

import type { Album } from '$lib/types/lastfm'
import type { LastClient } from '@musicorum/lastfm'
import { NowPlayingDetector, type NowPlayingUpdate } from './nowPlayingDetector'
import { AlbumEnricher } from './albumEnricher'
import { trackToAlbum, getAlbumKey } from './lastfmTransformers'
import { logger } from '$lib/server/logger'
// Type for Last.fm recent tracks response
interface RecentTracksResponse {
tracks: Array<{
name: string
album: {
name: string
mbid?: string
}
artist: {
name: string
}
nowPlaying?: boolean
date?: Date | { uts: number; '#text': string }
[key: string]: unknown
}>
[key: string]: unknown
}
export interface StreamState {
lastNowPlayingState: Map<string, { isPlaying: boolean; track?: string }>
lastAlbumOrder: string[]
}
export interface StreamUpdate {
albums?: Album[]
nowPlayingUpdates?: NowPlayingUpdate[]
}
export class LastfmStreamManager {
private client: LastClient
private username: string
private nowPlayingDetector: NowPlayingDetector
private albumEnricher: AlbumEnricher
private state: StreamState
constructor(client: LastClient, username: string) {
this.client = client
this.username = username
this.nowPlayingDetector = new NowPlayingDetector()
this.albumEnricher = new AlbumEnricher(client)
this.state = {
lastNowPlayingState: new Map(),
lastAlbumOrder: []
}
}
/**
* Check for updates and return any changes
*/
async checkForUpdates(): Promise<StreamUpdate> {
try {
// Always fetch fresh data for now playing detection
const freshData = await this.fetchFreshRecentTracks()
// Fetch recent albums
const albums = await this.getRecentAlbums(4, freshData)
// Process now playing status
await this.updateNowPlayingStatus(albums, freshData)
// Enrich albums with additional data
const enrichedAlbums = await this.enrichAlbums(albums)
// Ensure only one album is marked as now playing
this.ensureSingleNowPlaying(enrichedAlbums)
// Check for changes
const update: StreamUpdate = {}
// Check if album order or now playing status changed
if (this.hasAlbumsChanged(enrichedAlbums)) {
update.albums = enrichedAlbums
this.updateState(enrichedAlbums)
}
// Check for now playing updates for non-recent albums
const nowPlayingUpdates = await this.getNowPlayingUpdatesForNonRecentAlbums(enrichedAlbums, freshData)
if (nowPlayingUpdates.length > 0) {
update.nowPlayingUpdates = nowPlayingUpdates
}
return update
} catch (error) {
logger.error('Error checking for updates:', error as Error, undefined, 'music')
return {}
}
}
/**
* Fetch fresh recent tracks from Last.fm (no cache)
*/
private async fetchFreshRecentTracks(): Promise<RecentTracksResponse> {
logger.music('debug', 'Fetching fresh Last.fm recent tracks for now playing detection')
const recentTracksResponse = await this.client.user.getRecentTracks(this.username, {
limit: 50,
extended: true
})
// Still cache it for other uses, but always fetch fresh for now playing
await this.albumEnricher.cacheRecentTracks(this.username, recentTracksResponse)
return recentTracksResponse
}
/**
* Get recent albums from Last.fm
*/
private async getRecentAlbums(
limit: number,
recentTracksResponse?: RecentTracksResponse
): Promise<Album[]> {
// Use provided fresh data or fetch new
if (!recentTracksResponse) {
recentTracksResponse = await this.fetchFreshRecentTracks()
}
// Convert tracks to unique albums
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, trackToAlbum(track, uniqueAlbums.size + 1))
} else if (track.nowPlaying) {
// Update existing album if this track is now playing
const existingAlbum = uniqueAlbums.get(albumKey)!
uniqueAlbums.set(albumKey, {
...existingAlbum,
isNowPlaying: true,
nowPlayingTrack: track.name
})
}
}
return Array.from(uniqueAlbums.values())
}
/**
* Update now playing status using the detector
*/
private async updateNowPlayingStatus(
albums: Album[],
recentTracksResponse?: RecentTracksResponse
): Promise<void> {
// Use provided fresh data or fetch new
if (!recentTracksResponse) {
recentTracksResponse = await this.fetchFreshRecentTracks()
}
// Process now playing detection
const nowPlayingMap = await this.nowPlayingDetector.processNowPlayingTracks(
recentTracksResponse,
(artistName, albumName) =>
this.albumEnricher.getAppleMusicDataForNowPlaying(artistName, albumName)
)
// Update albums with now playing status
for (const album of albums) {
const key = getAlbumKey(album.artist.name, album.name)
const nowPlayingInfo = nowPlayingMap.get(key)
if (nowPlayingInfo) {
album.isNowPlaying = nowPlayingInfo.isNowPlaying
album.nowPlayingTrack = nowPlayingInfo.nowPlayingTrack
}
}
}
/**
* Enrich albums with additional data
*/
private async enrichAlbums(albums: Album[]): Promise<Album[]> {
return Promise.all(albums.map((album) => this.albumEnricher.enrichAlbum(album)))
}
/**
* Ensure only one album is marked as now playing
*/
private ensureSingleNowPlaying(albums: Album[]): void {
const nowPlayingCount = albums.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`
)
// Keep only the first now playing album (albums are already sorted by recency)
let foundFirst = false
albums.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 albums have changed
*/
private hasAlbumsChanged(albums: Album[]): boolean {
// Check album order
const currentAlbumOrder = albums.map((a) => getAlbumKey(a.artist.name, a.name))
const albumOrderChanged =
JSON.stringify(currentAlbumOrder) !== JSON.stringify(this.state.lastAlbumOrder)
// Check now playing status
let nowPlayingChanged = false
for (const album of albums) {
const key = getAlbumKey(album.artist.name, album.name)
const lastState = this.state.lastNowPlayingState.get(key)
if (
album.isNowPlaying !== (lastState?.isPlaying || false) ||
(album.isNowPlaying && album.nowPlayingTrack !== lastState?.track)
) {
nowPlayingChanged = true
break
}
}
return albumOrderChanged || nowPlayingChanged
}
/**
* Update internal state
*/
private updateState(albums: Album[]): void {
this.state.lastAlbumOrder = albums.map((a) => getAlbumKey(a.artist.name, a.name))
for (const album of albums) {
const key = getAlbumKey(album.artist.name, album.name)
this.state.lastNowPlayingState.set(key, {
isPlaying: album.isNowPlaying || false,
track: album.nowPlayingTrack
})
}
}
/**
* Get now playing updates for albums not in the recent list
*/
private async getNowPlayingUpdatesForNonRecentAlbums(
recentAlbums: Album[],
recentTracksResponse?: RecentTracksResponse
): Promise<NowPlayingUpdate[]> {
const updates: NowPlayingUpdate[] = []
// Use provided fresh data or fetch new
if (!recentTracksResponse) {
recentTracksResponse = await this.fetchFreshRecentTracks()
}
const nowPlayingMap = await this.nowPlayingDetector.processNowPlayingTracks(
recentTracksResponse,
(artistName, albumName) =>
this.albumEnricher.getAppleMusicDataForNowPlaying(artistName, albumName)
)
// Find albums that are now playing but not in recent albums
for (const [key, nowPlayingInfo] of nowPlayingMap) {
const isInRecentAlbums = recentAlbums.some((a) => getAlbumKey(a.artist.name, a.name) === key)
if (!isInRecentAlbums) {
const lastState = this.state.lastNowPlayingState.get(key)
const wasPlaying = lastState?.isPlaying || false
const lastTrack = lastState?.track
// Update if playing status changed OR if the track changed
if (
nowPlayingInfo.isNowPlaying !== wasPlaying ||
(nowPlayingInfo.isNowPlaying && nowPlayingInfo.nowPlayingTrack !== lastTrack)
) {
updates.push(nowPlayingInfo)
logger.music(
'debug',
`Now playing update for non-recent album ${nowPlayingInfo.albumName}: playing=${nowPlayingInfo.isNowPlaying}, track=${nowPlayingInfo.nowPlayingTrack}`
)
}
this.state.lastNowPlayingState.set(key, {
isPlaying: nowPlayingInfo.isNowPlaying,
track: nowPlayingInfo.nowPlayingTrack
})
}
}
return updates
}
}