diff --git a/web-player/src/lib/api/schema.d.ts b/web-player/src/lib/api/schema.d.ts index 863dbff..1192919 100644 --- a/web-player/src/lib/api/schema.d.ts +++ b/web-player/src/lib/api/schema.d.ts @@ -942,6 +942,10 @@ export interface components { largeImageKeyName: null | string; ownerUserId: string; visibility: components["schemas"]["PlaylistVisibility"]; + /** Format: int32 */ + trackCount: number | string; + /** Format: double */ + durationSeconds: number | string; /** Format: date-time */ createdAt: string; /** Format: date-time */ diff --git a/web-player/src/lib/config.ts b/web-player/src/lib/config.ts index ae7f588..0cf4150 100644 --- a/web-player/src/lib/config.ts +++ b/web-player/src/lib/config.ts @@ -9,9 +9,7 @@ export const PLAYLIST_TRACKS_PAGE_SIZE = 200; export const LIBRARY_PICKER_PAGE_SIZE = 50; export const LIBRARY_PAGE_SIZE = 50; export const PLAYLISTS_PAGE_SIZE = 50; -export const PLAYLIST_SUMMARY_TRACKS_PAGE_SIZE = 500; export const PROFILE_PLAYLISTS_PAGE_SIZE = 50; -export const COUNT_ONLY_PAGE_SIZE = 1; export const EXPLORE_ALBUMS_PAGE_SIZE = 10; export const EXPLORE_ALBUMS_LIMIT = 18; export const HOME_MIXES_LIMIT = 6; diff --git a/web-player/src/lib/server/genres.ts b/web-player/src/lib/server/genres.ts new file mode 100644 index 0000000..a05412c --- /dev/null +++ b/web-player/src/lib/server/genres.ts @@ -0,0 +1,18 @@ +import type { ApiClient } from '$lib/server/api'; +import type { components } from '$lib/api/schema'; + +type GenreInfo = components['schemas']['GenreResponse']; + +const CACHE_TTL_MS = 5 * 60_000; + +let cached: { genres: GenreInfo[]; expiresAt: number } | null = null; + +export async function getGenres(api: ApiClient): Promise { + if (cached && cached.expiresAt > Date.now()) return cached.genres; + + const { data } = await api.GET('/genres'); + if (!data) return cached?.genres ?? []; + + cached = { genres: data, expiresAt: Date.now() + CACHE_TTL_MS }; + return data; +} diff --git a/web-player/src/lib/server/mappers.test.ts b/web-player/src/lib/server/mappers.test.ts index 5e1f0dc..93ec898 100644 --- a/web-player/src/lib/server/mappers.test.ts +++ b/web-player/src/lib/server/mappers.test.ts @@ -1,15 +1,6 @@ import { describe, expect, it } from 'vitest'; import type { components } from '$lib/api/schema'; -import { - toAlbum, - toDatedTrack, - toLikedTrack, - toMix, - toPage, - toPlaylist, - toPlaylistSummary, - toTrack -} from './mappers'; +import { toAlbum, toDatedTrack, toLikedTrack, toMix, toPage, toPlaylist, toTrack } from './mappers'; const trackDto: components['schemas']['TrackApplicationResponse'] = { id: 't1', @@ -107,24 +98,25 @@ describe('toPlaylist', () => { largeImageKeyName: null, ownerUserId: '42', visibility: 'Public', + trackCount: '7', + durationSeconds: '90.5', createdAt: '', updatedAt: 'u', coverTrackIds: [] }; - it('drops storage keys', () => { + it('drops storage keys and normalizes totals', () => { expect(toPlaylist(dto)).toEqual({ id: 'p1', name: 'List', description: 'd', visibility: 'Public', ownerUserId: '42', + trackCount: 7, + durationSeconds: 90.5, coverTrackIds: [], updatedAt: 'u' }); }); - it('adds the track count in summaries', () => { - expect(toPlaylistSummary(dto, 7).trackCount).toBe(7); - }); }); describe('toPage', () => { diff --git a/web-player/src/lib/server/mappers.ts b/web-player/src/lib/server/mappers.ts index d94585b..cfd1175 100644 --- a/web-player/src/lib/server/mappers.ts +++ b/web-player/src/lib/server/mappers.ts @@ -1,5 +1,5 @@ import type { components } from '$lib/api/schema'; -import type { Album, LikedTrack, Mix, Paged, Playlist, PlaylistSummary, Track } from '$lib/types'; +import type { Album, LikedTrack, Mix, Paged, Playlist, Track } from '$lib/types'; type TrackDto = components['schemas']['TrackApplicationResponse']; type AlbumDto = components['schemas']['AlbumApplicationResponse']; @@ -75,15 +75,13 @@ export function toPlaylist(dto: PlaylistDto): Playlist { description: dto.description, visibility: dto.visibility, ownerUserId: dto.ownerUserId, + trackCount: Number(dto.trackCount) || 0, + durationSeconds: Number(dto.durationSeconds) || 0, coverTrackIds: dto.coverTrackIds, updatedAt: dto.updatedAt }; } -export function toPlaylistSummary(dto: PlaylistDto, trackCount: number): PlaylistSummary { - return { ...toPlaylist(dto), trackCount }; -} - export function toPage(dto: PageDto, map: (item: D) => T): Paged { return { items: dto.items.map(map), diff --git a/web-player/src/lib/types.ts b/web-player/src/lib/types.ts index e086f7f..fe43b2c 100644 --- a/web-player/src/lib/types.ts +++ b/web-player/src/lib/types.ts @@ -38,14 +38,12 @@ export interface Playlist { description: string | null; visibility: 'Public' | 'Private'; ownerUserId: string; + trackCount: number; + durationSeconds: number; coverTrackIds: string[]; updatedAt: string; } -export interface PlaylistSummary extends Playlist { - trackCount: number; -} - export interface Mix { id: string; title: string; diff --git a/web-player/src/routes/(app)/explore/+page.server.ts b/web-player/src/routes/(app)/explore/+page.server.ts index 05d1de5..4ea07e4 100644 --- a/web-player/src/routes/(app)/explore/+page.server.ts +++ b/web-player/src/routes/(app)/explore/+page.server.ts @@ -1,5 +1,6 @@ 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 { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions'; import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE, EXPLORE_USERS_LIMIT } from '$lib/config'; @@ -12,20 +13,17 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => { const user = locals.user; const api = apiFor({ fetch, locals }); - const genresRes = await api.GET('/genres'); - const genres = genresRes.data ?? []; - const genre = genres.find((item) => item.genre === genreParam)?.genre ?? null; - - const tracksPromise = api.GET('/tracks', { - params: { - query: { - name: query || undefined, - genre: genre ?? undefined, - pageNumber: page, - pageSize: EXPLORE_PAGE_SIZE + const tracksFor = (genre: string | null) => + api.GET('/tracks', { + params: { + query: { + name: query || undefined, + genre: genre ?? undefined, + pageNumber: page, + pageSize: EXPLORE_PAGE_SIZE + } } - } - }); + }); const albumsPromise = query ? api.GET('/albums', { @@ -39,11 +37,14 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => { }) : Promise.resolve(null); - const [tracksRes, albumsRes, usersRes] = await Promise.all([ - tracksPromise, + const [genres, firstTracksRes, albumsRes, usersRes] = await Promise.all([ + getGenres(api), + tracksFor(genreParam), albumsPromise, usersPromise ]); + const genre = genres.find((item) => item.genre === genreParam)?.genre ?? null; + const tracksRes = genre === genreParam ? firstTracksRes : await tracksFor(genre); const tracks = toPage(unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.'), (item) => toTrack(item.track) diff --git a/web-player/src/routes/(app)/playlists/+page.server.ts b/web-player/src/routes/(app)/playlists/+page.server.ts index 6538e5e..f3ad2e9 100644 --- a/web-player/src/routes/(app)/playlists/+page.server.ts +++ b/web-player/src/routes/(app)/playlists/+page.server.ts @@ -1,8 +1,8 @@ import type { PageServerLoad, Actions } from './$types'; import { fail, redirect } from '@sveltejs/kit'; import { apiFor, authedAction, requireUser, unwrapOrError } from '$lib/server/api'; -import { PLAYLISTS_PAGE_SIZE, PLAYLIST_SUMMARY_TRACKS_PAGE_SIZE } from '$lib/config'; -import { toPlaylistSummary, toTrack } from '$lib/server/mappers'; +import { PLAYLISTS_PAGE_SIZE } from '$lib/config'; +import { toPlaylist } from '$lib/server/mappers'; import { parsePlaylistForm } from '$lib/server/forms/playlistForm'; import { uploadOptionalCover } from '$lib/server/upload'; @@ -13,32 +13,17 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => { params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PLAYLISTS_PAGE_SIZE } } }); - const data = unwrapOrError(result, 'No se pudieron cargar tus playlists.'); - - const tracksByPlaylist = await Promise.all( - data.items.map((playlist) => - api - .GET('/playlists/{playlistId}/tracks', { - params: { - path: { playlistId: playlist.id }, - query: { pageNumber: 1, pageSize: PLAYLIST_SUMMARY_TRACKS_PAGE_SIZE } - } - }) - .then((res) => (res.data?.items ?? []).map(toTrack)) - ) - ); - - const items = data.items.map((playlist, i) => - toPlaylistSummary(playlist, tracksByPlaylist[i].length) + const playlists = unwrapOrError(result, 'No se pudieron cargar tus playlists.').items.map( + toPlaylist ); const totals = { - playlistCount: items.length, - trackCount: items.reduce((sum, playlist) => sum + playlist.trackCount, 0), - durationSeconds: tracksByPlaylist.flat().reduce((sum, track) => sum + track.duration, 0) + playlistCount: playlists.length, + trackCount: playlists.reduce((sum, playlist) => sum + playlist.trackCount, 0), + durationSeconds: playlists.reduce((sum, playlist) => sum + playlist.durationSeconds, 0) }; - return { playlists: items, totals }; + return { playlists, totals }; }; export const actions: Actions = { 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 4133a25..90947bf 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, optionalUser, unwrapOrError } from '$lib/server/api'; -import { toPlaylistSummary } from '$lib/server/mappers'; -import { COUNT_ONLY_PAGE_SIZE, PROFILE_PLAYLISTS_PAGE_SIZE } from '$lib/config'; +import { toPlaylist } from '$lib/server/mappers'; +import { PROFILE_PLAYLISTS_PAGE_SIZE } from '$lib/config'; import { followUserAction, unfollowUserAction } from '$lib/server/followActions'; export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => { @@ -20,24 +20,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent ]); const profile = unwrapOrError(profileRes, 'Usuario no encontrado.', 404); - const playlistItems = playlistsRes.data?.items ?? []; - - const tracksByPlaylist = await Promise.all( - playlistItems.map((playlist) => - api - .GET('/playlists/{playlistId}/tracks', { - params: { - path: { playlistId: playlist.id }, - query: { pageNumber: 1, pageSize: COUNT_ONLY_PAGE_SIZE } - } - }) - .then((res) => Number(res.data?.totalItemCount ?? 0)) - ) - ); - - const playlists = playlistItems.map((playlist, i) => - toPlaylistSummary(playlist, tracksByPlaylist[i]) - ); + const playlists = (playlistsRes.data?.items ?? []).map(toPlaylist); const isOwnProfile = viewer !== null && viewer.sub === params.id; diff --git a/web-player/src/routes/+page.svelte b/web-player/src/routes/+page.svelte index ac6d985..93f3a83 100644 --- a/web-player/src/routes/+page.svelte +++ b/web-player/src/routes/+page.svelte @@ -155,7 +155,7 @@ {@const meta = totalCount === 0 ? visibilityLabel - : `${plural(totalCount, 'canción', 'canciones')} · ${visibilityLabel}`} + : `${plural(totalCount, 'canción', 'canciones')} · ${fmtDurationLong(spotlight.durationSeconds)} · ${visibilityLabel}`}