web-player: use playlist totals from the API, cache genres and parallelize explore requests
This commit is contained in:
Vendored
+4
@@ -942,6 +942,10 @@ export interface components {
|
|||||||
largeImageKeyName: null | string;
|
largeImageKeyName: null | string;
|
||||||
ownerUserId: string;
|
ownerUserId: string;
|
||||||
visibility: components["schemas"]["PlaylistVisibility"];
|
visibility: components["schemas"]["PlaylistVisibility"];
|
||||||
|
/** Format: int32 */
|
||||||
|
trackCount: number | string;
|
||||||
|
/** Format: double */
|
||||||
|
durationSeconds: number | string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
/** Format: date-time */
|
/** Format: date-time */
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ export const PLAYLIST_TRACKS_PAGE_SIZE = 200;
|
|||||||
export const LIBRARY_PICKER_PAGE_SIZE = 50;
|
export const LIBRARY_PICKER_PAGE_SIZE = 50;
|
||||||
export const LIBRARY_PAGE_SIZE = 50;
|
export const LIBRARY_PAGE_SIZE = 50;
|
||||||
export const PLAYLISTS_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 PROFILE_PLAYLISTS_PAGE_SIZE = 50;
|
||||||
export const COUNT_ONLY_PAGE_SIZE = 1;
|
|
||||||
export const EXPLORE_ALBUMS_PAGE_SIZE = 10;
|
export const EXPLORE_ALBUMS_PAGE_SIZE = 10;
|
||||||
export const EXPLORE_ALBUMS_LIMIT = 18;
|
export const EXPLORE_ALBUMS_LIMIT = 18;
|
||||||
export const HOME_MIXES_LIMIT = 6;
|
export const HOME_MIXES_LIMIT = 6;
|
||||||
|
|||||||
@@ -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<GenreInfo[]> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
@@ -1,15 +1,6 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { components } from '$lib/api/schema';
|
import type { components } from '$lib/api/schema';
|
||||||
import {
|
import { toAlbum, toDatedTrack, toLikedTrack, toMix, toPage, toPlaylist, toTrack } from './mappers';
|
||||||
toAlbum,
|
|
||||||
toDatedTrack,
|
|
||||||
toLikedTrack,
|
|
||||||
toMix,
|
|
||||||
toPage,
|
|
||||||
toPlaylist,
|
|
||||||
toPlaylistSummary,
|
|
||||||
toTrack
|
|
||||||
} from './mappers';
|
|
||||||
|
|
||||||
const trackDto: components['schemas']['TrackApplicationResponse'] = {
|
const trackDto: components['schemas']['TrackApplicationResponse'] = {
|
||||||
id: 't1',
|
id: 't1',
|
||||||
@@ -107,24 +98,25 @@ describe('toPlaylist', () => {
|
|||||||
largeImageKeyName: null,
|
largeImageKeyName: null,
|
||||||
ownerUserId: '42',
|
ownerUserId: '42',
|
||||||
visibility: 'Public',
|
visibility: 'Public',
|
||||||
|
trackCount: '7',
|
||||||
|
durationSeconds: '90.5',
|
||||||
createdAt: '',
|
createdAt: '',
|
||||||
updatedAt: 'u',
|
updatedAt: 'u',
|
||||||
coverTrackIds: []
|
coverTrackIds: []
|
||||||
};
|
};
|
||||||
it('drops storage keys', () => {
|
it('drops storage keys and normalizes totals', () => {
|
||||||
expect(toPlaylist(dto)).toEqual({
|
expect(toPlaylist(dto)).toEqual({
|
||||||
id: 'p1',
|
id: 'p1',
|
||||||
name: 'List',
|
name: 'List',
|
||||||
description: 'd',
|
description: 'd',
|
||||||
visibility: 'Public',
|
visibility: 'Public',
|
||||||
ownerUserId: '42',
|
ownerUserId: '42',
|
||||||
|
trackCount: 7,
|
||||||
|
durationSeconds: 90.5,
|
||||||
coverTrackIds: [],
|
coverTrackIds: [],
|
||||||
updatedAt: 'u'
|
updatedAt: 'u'
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
it('adds the track count in summaries', () => {
|
|
||||||
expect(toPlaylistSummary(dto, 7).trackCount).toBe(7);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('toPage', () => {
|
describe('toPage', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { components } from '$lib/api/schema';
|
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 TrackDto = components['schemas']['TrackApplicationResponse'];
|
||||||
type AlbumDto = components['schemas']['AlbumApplicationResponse'];
|
type AlbumDto = components['schemas']['AlbumApplicationResponse'];
|
||||||
@@ -75,15 +75,13 @@ export function toPlaylist(dto: PlaylistDto): Playlist {
|
|||||||
description: dto.description,
|
description: dto.description,
|
||||||
visibility: dto.visibility,
|
visibility: dto.visibility,
|
||||||
ownerUserId: dto.ownerUserId,
|
ownerUserId: dto.ownerUserId,
|
||||||
|
trackCount: Number(dto.trackCount) || 0,
|
||||||
|
durationSeconds: Number(dto.durationSeconds) || 0,
|
||||||
coverTrackIds: dto.coverTrackIds,
|
coverTrackIds: dto.coverTrackIds,
|
||||||
updatedAt: dto.updatedAt
|
updatedAt: dto.updatedAt
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toPlaylistSummary(dto: PlaylistDto, trackCount: number): PlaylistSummary {
|
|
||||||
return { ...toPlaylist(dto), trackCount };
|
|
||||||
}
|
|
||||||
|
|
||||||
export function toPage<D, T>(dto: PageDto<D>, map: (item: D) => T): Paged<T> {
|
export function toPage<D, T>(dto: PageDto<D>, map: (item: D) => T): Paged<T> {
|
||||||
return {
|
return {
|
||||||
items: dto.items.map(map),
|
items: dto.items.map(map),
|
||||||
|
|||||||
@@ -38,14 +38,12 @@ export interface Playlist {
|
|||||||
description: string | null;
|
description: string | null;
|
||||||
visibility: 'Public' | 'Private';
|
visibility: 'Public' | 'Private';
|
||||||
ownerUserId: string;
|
ownerUserId: string;
|
||||||
|
trackCount: number;
|
||||||
|
durationSeconds: number;
|
||||||
coverTrackIds: string[];
|
coverTrackIds: string[];
|
||||||
updatedAt: string;
|
updatedAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PlaylistSummary extends Playlist {
|
|
||||||
trackCount: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Mix {
|
export interface Mix {
|
||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { apiFor, unwrapOrError } from '$lib/server/api';
|
import { apiFor, unwrapOrError } from '$lib/server/api';
|
||||||
|
import { getGenres } from '$lib/server/genres';
|
||||||
import { toAlbum, toPage, toTrack } from '$lib/server/mappers';
|
import { toAlbum, toPage, toTrack } from '$lib/server/mappers';
|
||||||
import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions';
|
import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions';
|
||||||
import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE, EXPLORE_USERS_LIMIT } from '$lib/config';
|
import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE, EXPLORE_USERS_LIMIT } from '$lib/config';
|
||||||
@@ -12,11 +13,8 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
|||||||
const user = locals.user;
|
const user = locals.user;
|
||||||
const api = apiFor({ fetch, locals });
|
const api = apiFor({ fetch, locals });
|
||||||
|
|
||||||
const genresRes = await api.GET('/genres');
|
const tracksFor = (genre: string | null) =>
|
||||||
const genres = genresRes.data ?? [];
|
api.GET('/tracks', {
|
||||||
const genre = genres.find((item) => item.genre === genreParam)?.genre ?? null;
|
|
||||||
|
|
||||||
const tracksPromise = api.GET('/tracks', {
|
|
||||||
params: {
|
params: {
|
||||||
query: {
|
query: {
|
||||||
name: query || undefined,
|
name: query || undefined,
|
||||||
@@ -39,11 +37,14 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
|||||||
})
|
})
|
||||||
: Promise.resolve(null);
|
: Promise.resolve(null);
|
||||||
|
|
||||||
const [tracksRes, albumsRes, usersRes] = await Promise.all([
|
const [genres, firstTracksRes, albumsRes, usersRes] = await Promise.all([
|
||||||
tracksPromise,
|
getGenres(api),
|
||||||
|
tracksFor(genreParam),
|
||||||
albumsPromise,
|
albumsPromise,
|
||||||
usersPromise
|
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) =>
|
const tracks = toPage(unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.'), (item) =>
|
||||||
toTrack(item.track)
|
toTrack(item.track)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { fail, redirect } from '@sveltejs/kit';
|
import { fail, redirect } from '@sveltejs/kit';
|
||||||
import { apiFor, authedAction, requireUser, unwrapOrError } from '$lib/server/api';
|
import { apiFor, authedAction, requireUser, unwrapOrError } from '$lib/server/api';
|
||||||
import { PLAYLISTS_PAGE_SIZE, PLAYLIST_SUMMARY_TRACKS_PAGE_SIZE } from '$lib/config';
|
import { PLAYLISTS_PAGE_SIZE } from '$lib/config';
|
||||||
import { toPlaylistSummary, toTrack } from '$lib/server/mappers';
|
import { toPlaylist } from '$lib/server/mappers';
|
||||||
import { parsePlaylistForm } from '$lib/server/forms/playlistForm';
|
import { parsePlaylistForm } from '$lib/server/forms/playlistForm';
|
||||||
import { uploadOptionalCover } from '$lib/server/upload';
|
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 } }
|
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PLAYLISTS_PAGE_SIZE } }
|
||||||
});
|
});
|
||||||
|
|
||||||
const data = unwrapOrError(result, 'No se pudieron cargar tus playlists.');
|
const playlists = unwrapOrError(result, 'No se pudieron cargar tus playlists.').items.map(
|
||||||
|
toPlaylist
|
||||||
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 totals = {
|
const totals = {
|
||||||
playlistCount: items.length,
|
playlistCount: playlists.length,
|
||||||
trackCount: items.reduce((sum, playlist) => sum + playlist.trackCount, 0),
|
trackCount: playlists.reduce((sum, playlist) => sum + playlist.trackCount, 0),
|
||||||
durationSeconds: tracksByPlaylist.flat().reduce((sum, track) => sum + track.duration, 0)
|
durationSeconds: playlists.reduce((sum, playlist) => sum + playlist.durationSeconds, 0)
|
||||||
};
|
};
|
||||||
|
|
||||||
return { playlists: items, totals };
|
return { playlists, totals };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions: Actions = {
|
export const actions: Actions = {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import type { PageServerLoad, Actions } from './$types';
|
import type { PageServerLoad, Actions } from './$types';
|
||||||
import { apiFor, optionalUser, unwrapOrError } from '$lib/server/api';
|
import { apiFor, optionalUser, unwrapOrError } from '$lib/server/api';
|
||||||
import { toPlaylistSummary } from '$lib/server/mappers';
|
import { toPlaylist } from '$lib/server/mappers';
|
||||||
import { COUNT_ONLY_PAGE_SIZE, PROFILE_PLAYLISTS_PAGE_SIZE } from '$lib/config';
|
import { PROFILE_PLAYLISTS_PAGE_SIZE } from '$lib/config';
|
||||||
import { followUserAction, unfollowUserAction } from '$lib/server/followActions';
|
import { followUserAction, unfollowUserAction } from '$lib/server/followActions';
|
||||||
|
|
||||||
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
|
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 profile = unwrapOrError(profileRes, 'Usuario no encontrado.', 404);
|
||||||
const playlistItems = playlistsRes.data?.items ?? [];
|
const playlists = (playlistsRes.data?.items ?? []).map(toPlaylist);
|
||||||
|
|
||||||
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 isOwnProfile = viewer !== null && viewer.sub === params.id;
|
const isOwnProfile = viewer !== null && viewer.sub === params.id;
|
||||||
|
|
||||||
|
|||||||
@@ -155,7 +155,7 @@
|
|||||||
{@const meta =
|
{@const meta =
|
||||||
totalCount === 0
|
totalCount === 0
|
||||||
? visibilityLabel
|
? visibilityLabel
|
||||||
: `${plural(totalCount, 'canción', 'canciones')} · ${visibilityLabel}`}
|
: `${plural(totalCount, 'canción', 'canciones')} · ${fmtDurationLong(spotlight.durationSeconds)} · ${visibilityLabel}`}
|
||||||
<div
|
<div
|
||||||
class="grid spotlight-surface grid-cols-1 overflow-hidden rounded-panel-lg lg:grid-cols-2"
|
class="grid spotlight-surface grid-cols-1 overflow-hidden rounded-panel-lg lg:grid-cols-2"
|
||||||
>
|
>
|
||||||
|
|||||||
Reference in New Issue
Block a user