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')); }); });