diff --git a/web-player/src/lib/server/mappers.test.ts b/web-player/src/lib/server/mappers.test.ts index 93ec898..c8e1d4e 100644 --- a/web-player/src/lib/server/mappers.test.ts +++ b/web-player/src/lib/server/mappers.test.ts @@ -1,6 +1,16 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$lib/api/schema'; -import { toAlbum, toDatedTrack, toLikedTrack, toMix, toPage, toPlaylist, toTrack } from './mappers'; +import { + toAlbum, + toCount, + toDatedTrack, + toLikedTrack, + toListeningStats, + toMix, + toPage, + toPlaylist, + toTrack +} from './mappers'; const trackDto: components['schemas']['TrackApplicationResponse'] = { id: 't1', @@ -129,3 +139,29 @@ describe('toPage', () => { ).toEqual({ items: [2, 4], pageNumber: 2, hasNextPage: true, totalItemCount: 10 }); }); }); + +describe('toCount', () => { + it('normalizes numbers, numeric strings and missing values', () => { + expect(toCount(4)).toBe(4); + expect(toCount('12')).toBe(12); + expect(toCount(undefined)).toBe(0); + expect(toCount(null)).toBe(0); + expect(toCount('abc')).toBe(0); + }); +}); + +describe('toListeningStats', () => { + it('converts string counters to numbers', () => { + expect( + toListeningStats({ tracksThisWeek: '5', secondsThisWeek: 300, streakDays: '2' }) + ).toEqual({ tracksThisWeek: 5, secondsThisWeek: 300, streakDays: 2 }); + }); + + it('falls back to zeros without data', () => { + expect(toListeningStats(undefined)).toEqual({ + tracksThisWeek: 0, + secondsThisWeek: 0, + streakDays: 0 + }); + }); +}); diff --git a/web-player/src/lib/server/mappers.ts b/web-player/src/lib/server/mappers.ts index cfd1175..b708925 100644 --- a/web-player/src/lib/server/mappers.ts +++ b/web-player/src/lib/server/mappers.ts @@ -82,6 +82,26 @@ export function toPlaylist(dto: PlaylistDto): Playlist { }; } +type ListeningStatsDto = components['schemas']['ListeningStatsResponse']; + +export interface ListeningStats { + tracksThisWeek: number; + secondsThisWeek: number; + streakDays: number; +} + +export function toCount(value: number | string | null | undefined): number { + return Number(value ?? 0) || 0; +} + +export function toListeningStats(dto: ListeningStatsDto | null | undefined): ListeningStats { + return { + tracksThisWeek: toCount(dto?.tracksThisWeek), + secondsThisWeek: toCount(dto?.secondsThisWeek), + streakDays: toCount(dto?.streakDays) + }; +} + export function toPage(dto: PageDto, map: (item: D) => T): Paged { return { items: dto.items.map(map), diff --git a/web-player/src/routes/(app)/+layout.server.ts b/web-player/src/routes/(app)/+layout.server.ts index 529d9ee..e8032fa 100644 --- a/web-player/src/routes/(app)/+layout.server.ts +++ b/web-player/src/routes/(app)/+layout.server.ts @@ -2,7 +2,7 @@ import type { LayoutServerLoad } from './$types'; import { getAllowAnonymousListening } from '$lib/server/playbackConfig'; import { apiFor } from '$lib/server/api'; import { loginRedirect } from '$lib/server/guards'; -import { toLikedTrack, toPlaylist, toTrack } from '$lib/server/mappers'; +import { toCount, toLikedTrack, toPlaylist, toTrack } from '$lib/server/mappers'; import { authConfig } from '$lib/server/config'; import { LIKED_TRACKS_PAGE_SIZE, PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config'; @@ -45,7 +45,7 @@ export const load: LayoutServerLoad = async ({ locals, url, fetch }) => { }) .then(({ data }) => ({ items: (data?.items ?? []).map(toPlaylist), - total: Number(data?.totalItemCount ?? 0) + total: toCount(data?.totalItemCount) })), { items: [], total: 0 } ), diff --git a/web-player/src/routes/(app)/+page.server.ts b/web-player/src/routes/(app)/+page.server.ts index 80f24c0..3f6fca1 100644 --- a/web-player/src/routes/(app)/+page.server.ts +++ b/web-player/src/routes/(app)/+page.server.ts @@ -2,7 +2,7 @@ import { redirect } from '@sveltejs/kit'; import type { PageServerLoad, Actions } from './$types'; import { apiFor } from '$lib/server/api'; import { requireUser } from '$lib/server/guards'; -import { toMix, toPlaylist, toTrack } from '$lib/server/mappers'; +import { toCount, toListeningStats, toMix, toPlaylist, toTrack } from '$lib/server/mappers'; import { addTrackAction } from '$lib/server/actions/playlist'; import { HOME_MIXES_LIMIT, HOME_SHELF_LIMIT, HOME_SPOTLIGHT_TRACKS_LIMIT } from '$lib/config'; import { pickGreeting } from '$lib/server/greeting'; @@ -24,12 +24,8 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => { }); const listeningStatsPromise = api .GET('/users/{id}/listening-stats', { params: { path: { id: user.sub } } }) - .then((res) => ({ - tracksThisWeek: Number(res.data?.tracksThisWeek ?? 0), - secondsThisWeek: Number(res.data?.secondsThisWeek ?? 0), - streakDays: Number(res.data?.streakDays ?? 0) - })) - .catch(() => ({ tracksThisWeek: 0, secondsThisWeek: 0, streakDays: 0 })); + .then((res) => toListeningStats(res.data)) + .catch(() => toListeningStats(null)); const [playlists, mixes, recentlyPlayed] = await Promise.all([ playlistsPromise, @@ -49,7 +45,7 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => { }) .then((res) => ({ items: (res.data?.items ?? []).map(toTrack), - totalCount: Number(res.data?.totalItemCount ?? 0) + totalCount: toCount(res.data?.totalItemCount) })) .catch(() => ({ items: [], totalCount: 0 })) : Promise.resolve({ items: [], totalCount: 0 }); diff --git a/web-player/src/routes/(app)/explore/+page.server.ts b/web-player/src/routes/(app)/explore/+page.server.ts index 8daa47d..12d4a0f 100644 --- a/web-player/src/routes/(app)/explore/+page.server.ts +++ b/web-player/src/routes/(app)/explore/+page.server.ts @@ -1,7 +1,7 @@ import type { PageServerLoad, Actions } from './$types'; import { apiFor, unwrapOrError } from '$lib/server/api'; import { getGenres } from '$lib/server/genres'; -import { toAlbum, toPage, toTrack } from '$lib/server/mappers'; +import { toAlbum, toCount, toPage, toTrack } from '$lib/server/mappers'; import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/actions/playlist'; import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE, EXPLORE_USERS_LIMIT } from '$lib/config'; @@ -56,9 +56,9 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => { genres, tracks, albums: (albumsRes?.data?.items ?? []).map((item) => toAlbum(item.album)), - albumsTotal: Number(albumsRes?.data?.totalItemCount ?? 0), + albumsTotal: toCount(albumsRes?.data?.totalItemCount), users: usersRes?.data?.items ?? [], - usersTotal: Number(usersRes?.data?.totalItemCount ?? 0), + usersTotal: toCount(usersRes?.data?.totalItemCount), viewerId: user?.sub ?? null }; }; diff --git a/web-player/src/routes/(app)/user/[id]/+page.server.ts b/web-player/src/routes/(app)/user/[id]/+page.server.ts index c0dc5de..779ff28 100644 --- a/web-player/src/routes/(app)/user/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/user/[id]/+page.server.ts @@ -1,7 +1,7 @@ import type { PageServerLoad, Actions } from './$types'; import { apiFor, unwrapOrError } from '$lib/server/api'; import { optionalUser } from '$lib/server/guards'; -import { toPlaylist } from '$lib/server/mappers'; +import { toCount, toPlaylist } from '$lib/server/mappers'; import { PROFILE_PLAYLISTS_PAGE_SIZE } from '$lib/config'; import { followUserAction, unfollowUserAction } from '$lib/server/actions/follow'; @@ -27,7 +27,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent return { profile, - followersCount: Number(profile.followersCount), + followersCount: toCount(profile.followersCount), playlists, isOwnProfile, isAnonymous: viewer === null