Files
website/src/lib/music/music.test.ts
Stan Runge b757269151
All checks were successful
Deploy production / deploy (push) Successful in 2m37s
music: self-hosted scrobbler (Spotify poller, Last.fm + streaming-history importers, /trackers/music, homepage listening card)
- music schema: artists/albums/tracks (+album art), plays with source + dedupe, now_playing,
  spotify_auth, import_state; plays_deduped view hides Last.fm scrobbles Spotify also has
- in-app Spotify poller (60s, leased) started from hooks init; no new role or env needed
- /api/spotify/connect + callback (owner only), /trackers/music public stats
- scripts: music-poll, music-import-lastfm (resumable), music-import-history
- deploy/music-collector: optional host timer with its own role
2026-10-24 15:57:40 +00:00

168 lines
5.5 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import { parseHistoryEntry } from './history';
import { parseRecentTracks } from './lastfm-parse';
import { matchKey } from './normalize';
import { parsePlayer, parseRecentlyPlayed, parseTrack, pickImages } from './spotify-parse';
const ID = '4uLU6hMCjMI75M1A2tKUQC';
const spotifyTrack = {
type: 'track',
id: ID,
name: 'Never Gonna Give You Up',
duration_ms: 213573,
explicit: false,
artists: [{ id: '0gxyHStUsqpMadRV0Di1Qt', name: 'Rick Astley' }],
album: {
id: '6N9PS4QXF1D0OWPk0Sxtb4',
name: 'Whenever You Need Somebody',
release_date: '1987-11-12',
images: [
{ url: 'https://i.scdn.co/image/640', width: 640, height: 640 },
{ url: 'https://i.scdn.co/image/300', width: 300, height: 300 },
{ url: 'https://i.scdn.co/image/64', width: 64, height: 64 }
]
}
};
describe('spotify parsing', () => {
it('parses a track with album art', () => {
const t = parseTrack(spotifyTrack)!;
expect(t.spotifyId).toBe(ID);
expect(t.artists).toEqual([{ spotifyId: '0gxyHStUsqpMadRV0Di1Qt', name: 'Rick Astley' }]);
expect(t.album?.imageUrl).toBe('https://i.scdn.co/image/640');
expect(t.album?.imageUrlSmall).toBe('https://i.scdn.co/image/300');
});
it('skips local files, episodes and nameless items', () => {
expect(parseTrack({ ...spotifyTrack, is_local: true })).toBeNull();
expect(parseTrack({ ...spotifyTrack, type: 'episode' })).toBeNull();
expect(parseTrack({ ...spotifyTrack, name: ' ' })).toBeNull();
});
it('keeps only https images', () => {
expect(pickImages([{ url: 'http://x/1', width: 640 }])).toEqual({ large: null, small: null });
});
it('sorts recently played oldest first', () => {
const plays = parseRecentlyPlayed({
items: [
{
track: spotifyTrack,
played_at: '2026-10-24T15:00:00.123Z',
context: { type: 'playlist', uri: 'spotify:playlist:x' }
},
{ track: spotifyTrack, played_at: '2026-10-24T14:00:00.000Z', context: null },
{ track: null, played_at: '2026-10-24T13:00:00.000Z' }
]
});
expect(plays.map((p) => p.playedAt.toISOString())).toEqual([
'2026-10-24T14:00:00.000Z',
'2026-10-24T15:00:00.123Z'
]);
expect(plays[1].contextType).toBe('playlist');
});
it('parses the player and ignores podcasts', () => {
expect(parsePlayer(null)).toBeNull();
const p = parsePlayer({
is_playing: true,
progress_ms: 1000,
currently_playing_type: 'track',
item: spotifyTrack,
device: { name: 'Mac', type: 'Computer' }
})!;
expect(p).toMatchObject({ isPlaying: true, deviceName: 'Mac' });
expect(p.track?.name).toBe('Never Gonna Give You Up');
expect(
parsePlayer({ is_playing: true, currently_playing_type: 'episode', item: {} })!.track
).toBeNull();
});
});
describe('history export parsing', () => {
it('parses extended streaming history and skips short plays and podcasts', () => {
const play = parseHistoryEntry({
ts: '2024-01-02T03:04:05Z',
ms_played: 200000,
platform: 'ios',
master_metadata_track_name: 'Song',
master_metadata_album_artist_name: 'Artist',
master_metadata_album_album_name: 'Album',
spotify_track_uri: `spotify:track:${ID}`
})!;
expect(play.track.spotifyId).toBe(ID);
expect(play.playedAt.toISOString()).toBe('2024-01-02T03:04:05.000Z');
expect(
parseHistoryEntry({
ts: '2024-01-02T03:04:05Z',
ms_played: 5000,
master_metadata_track_name: 'S',
master_metadata_album_artist_name: 'A'
})
).toBeNull();
expect(
parseHistoryEntry({
ts: '2024-01-02T03:04:05Z',
ms_played: 900000,
master_metadata_track_name: null,
episode_name: 'Pod'
})
).toBeNull();
});
it('parses the account-data export (UTC endTime)', () => {
const play = parseHistoryEntry({
endTime: '2023-05-06 07:08',
artistName: 'A',
trackName: 'T',
msPlayed: 60000
})!;
expect(play.playedAt.toISOString()).toBe('2023-05-06T07:08:00.000Z');
expect(play.track.spotifyId).toBeNull();
});
});
describe('last.fm parsing', () => {
it('skips the now-playing item and placeholder covers', () => {
const page = parseRecentTracks({
recenttracks: {
'@attr': { page: '1', totalPages: '3', total: '500' },
track: [
{ '@attr': { nowplaying: 'true' }, name: 'Now', artist: { '#text': 'A' } },
{
name: 'Song',
artist: { '#text': 'Artist' },
album: { '#text': 'Album' },
date: { uts: '1700000000' },
image: [
{ size: 'large', '#text': 'https://lastfm.freetls.fastly.net/i/u/174s/abc.png' },
{
size: 'extralarge',
'#text': 'https://lastfm.freetls.fastly.net/i/u/300x300/abc.png'
}
]
},
{
name: 'Starless',
artist: { '#text': 'B' },
date: { uts: '1690000000' },
image: [{ size: 'large', '#text': 'https://x/2a96cbd8b46e442fc41c2b86b821562f.png' }]
}
]
}
});
expect(page.totalPages).toBe(3);
expect(page.plays).toHaveLength(2);
expect(page.plays[0].playedAt.toISOString()).toBe('2023-11-14T22:13:20.000Z');
expect(page.plays[0].track.album?.imageUrl).toContain('300x300');
expect(page.plays[1].track.album).toBeNull();
});
});
describe('matchKey', () => {
it('ignores remaster/feat/accents/punctuation differences', () => {
const base = matchKey('Beyoncé', 'Crazy in Love');
expect(matchKey('Beyonce', 'Crazy In Love (feat. Jay-Z)')).toBe(base);
expect(matchKey('BEYONCÉ', 'Crazy in Love - 2003 Remaster')).toBe(base);
expect(matchKey('Simon & Garfunkel', 'The Boxer')).toBe(
matchKey('Simon and Garfunkel', 'The Boxer')
);
expect(matchKey('A', 'Song - Part 2')).not.toBe(matchKey('A', 'Song'));
});
});