Merge branch 'backend-refactor-rev' into audit-merge-three
Adapt the refactored web player to the new backend API (id path params, bulk track adds, flat list items, mix kind) and port AddAlbumToPlayList to the backend conventions.
This commit is contained in:
Vendored
+564
-644
File diff suppressed because it is too large
Load Diff
@@ -4,8 +4,8 @@ export const SEARCH_DEBOUNCE_MS = 450;
|
||||
export const SEARCH_MIN_LENGTH = 2;
|
||||
export const INFINITE_SCROLL_ROOT_MARGIN = '300px';
|
||||
export const ALBUMS_PAGE_SIZE = 50;
|
||||
export const ALBUM_TRACKS_PAGE_SIZE = 200;
|
||||
export const PLAYLIST_TRACKS_PAGE_SIZE = 200;
|
||||
export const ALBUM_TRACKS_PAGE_SIZE = 100;
|
||||
export const PLAYLIST_TRACKS_LIMIT = 500;
|
||||
export const LIBRARY_PICKER_PAGE_SIZE = 50;
|
||||
export const LIBRARY_PAGE_SIZE = 50;
|
||||
export const PLAYLISTS_PAGE_SIZE = 50;
|
||||
@@ -17,7 +17,7 @@ export const HOME_SHELF_LIMIT = 6;
|
||||
export const HOME_CONTINUE_LIMIT = 10;
|
||||
export const HOME_SPOTLIGHT_TRACKS_LIMIT = 5;
|
||||
export const PLAYLIST_PICKER_PAGE_SIZE = 50;
|
||||
export const LIKED_TRACKS_PAGE_SIZE = 500;
|
||||
export const LIKED_TRACKS_LIMIT = 500;
|
||||
export const EXPLORE_USERS_LIMIT = 12;
|
||||
export const FOLLOW_LIST_PAGE_SIZE = 30;
|
||||
export const MENU_WIDTH = 240;
|
||||
|
||||
@@ -10,15 +10,12 @@ export const addTrackAction = authedAction(async ({ api, form, params }) => {
|
||||
return fail(400, { message: 'Faltan datos de la canción.' });
|
||||
}
|
||||
|
||||
const { error, response } = await api.POST('/playlists/{playlistId}/tracks', {
|
||||
params: { path: { playlistId } },
|
||||
body: { trackId }
|
||||
const { error } = await api.POST('/playlists/{id}/tracks', {
|
||||
params: { path: { id: playlistId } },
|
||||
body: { trackIds: [trackId] }
|
||||
});
|
||||
|
||||
if (error) {
|
||||
if (response?.status === 409) {
|
||||
return fail(409, { message: 'La canción ya está en esa playlist.', trackId });
|
||||
}
|
||||
return fail(502, { message: 'No se pudo añadir la canción.', trackId });
|
||||
}
|
||||
|
||||
@@ -33,8 +30,8 @@ export const addAlbumToPlaylistAction = authedAction(async ({ api, form, params
|
||||
return fail(400, { message: 'Faltan datos del álbum.' });
|
||||
}
|
||||
|
||||
const { data, error } = await api.POST('/playlists/{playlistId}/albums/{albumId}', {
|
||||
params: { path: { playlistId, albumId } }
|
||||
const { data, error } = await api.POST('/playlists/{id}/albums/{albumId}', {
|
||||
params: { path: { id: playlistId, albumId } }
|
||||
});
|
||||
|
||||
if (error || !data) {
|
||||
|
||||
@@ -56,11 +56,11 @@ describe('toMix', () => {
|
||||
it('maps items into tracks', () => {
|
||||
const mix = toMix({
|
||||
id: 'm1',
|
||||
title: 'Mix',
|
||||
subtitle: null,
|
||||
kind: 'Daily',
|
||||
itemCount: 1,
|
||||
items: [{ trackId: 't1', title: 'Song', artist: 'A', durationSeconds: '60', listensCount: 3 }]
|
||||
});
|
||||
expect(mix.title).toBe('Tu mezcla diaria');
|
||||
expect(mix.tracks).toEqual([
|
||||
{
|
||||
id: 't1',
|
||||
|
||||
@@ -46,11 +46,21 @@ export function toMixTrack(item: MixItemDto): Track {
|
||||
};
|
||||
}
|
||||
|
||||
const MIX_TEXTS: Record<MixDto['kind'], { title: string; subtitle: string }> = {
|
||||
Discovery: {
|
||||
title: 'Descubrimiento',
|
||||
subtitle: 'Canciones de tu biblioteca que todavía no has escuchado.'
|
||||
},
|
||||
Daily: {
|
||||
title: 'Tu mezcla diaria',
|
||||
subtitle: 'Lo que más escuchas, con alguna sorpresa de tu biblioteca.'
|
||||
}
|
||||
};
|
||||
|
||||
export function toMix(dto: MixDto): Mix {
|
||||
return {
|
||||
id: dto.id,
|
||||
title: dto.title,
|
||||
subtitle: dto.subtitle,
|
||||
...MIX_TEXTS[dto.kind],
|
||||
tracks: dto.items.map(toMixTrack)
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
/** Largest page size the API accepts (`PageRequest.MaxPageSize` in the backend). */
|
||||
export const API_MAX_PAGE_SIZE = 100;
|
||||
|
||||
type Page<T> = { items: T[]; hasNextPage: boolean };
|
||||
|
||||
/** Loads consecutive pages until the list ends or `maxItems` items were collected. */
|
||||
export async function fetchAllPages<T>(
|
||||
loadPage: (pageNumber: number, pageSize: number) => Promise<Page<T> | undefined>,
|
||||
maxItems: number
|
||||
): Promise<T[]> {
|
||||
const items: T[] = [];
|
||||
|
||||
for (let pageNumber = 1; items.length < maxItems; pageNumber++) {
|
||||
const page = await loadPage(pageNumber, API_MAX_PAGE_SIZE);
|
||||
if (!page) break;
|
||||
|
||||
items.push(...page.items);
|
||||
if (!page.hasNextPage) break;
|
||||
}
|
||||
|
||||
return items.slice(0, maxItems);
|
||||
}
|
||||
@@ -4,7 +4,8 @@ import { apiFor } from '$lib/server/api';
|
||||
import { loginRedirect } from '$lib/server/guards';
|
||||
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';
|
||||
import { fetchAllPages } from '$lib/server/pagination';
|
||||
import { LIKED_TRACKS_LIMIT, PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config';
|
||||
|
||||
function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
|
||||
return load.catch(() => fallback);
|
||||
@@ -50,11 +51,11 @@ export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
|
||||
{ items: [], total: 0 }
|
||||
),
|
||||
orFallback(
|
||||
api
|
||||
.GET('/likes', {
|
||||
params: { query: { pageNumber: 1, pageSize: LIKED_TRACKS_PAGE_SIZE } }
|
||||
})
|
||||
.then(({ data }) => (data?.items ?? []).map(toLikedTrack)),
|
||||
fetchAllPages(
|
||||
async (pageNumber, pageSize) =>
|
||||
(await api.GET('/likes', { params: { query: { pageNumber, pageSize } } })).data,
|
||||
LIKED_TRACKS_LIMIT
|
||||
).then((items) => items.map(toLikedTrack)),
|
||||
[]
|
||||
),
|
||||
orFallback(
|
||||
|
||||
@@ -37,9 +37,9 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||
const spotlightPlaylist = playlistItems[0] ?? null;
|
||||
const spotlightTracksPromise = spotlightPlaylist
|
||||
? api
|
||||
.GET('/playlists/{playlistId}/tracks', {
|
||||
.GET('/playlists/{id}/tracks', {
|
||||
params: {
|
||||
path: { playlistId: spotlightPlaylist.id },
|
||||
path: { id: spotlightPlaylist.id },
|
||||
query: { pageNumber: 1, pageSize: HOME_SPOTLIGHT_TRACKS_LIMIT }
|
||||
}
|
||||
})
|
||||
|
||||
@@ -16,9 +16,9 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
|
||||
const [albumRes, tracksRes, libraryRes] = await Promise.all([
|
||||
api.GET('/albums/{id}', { params: { path: { id: params.id } } }),
|
||||
api.GET('/albums/{albumId}/tracks', {
|
||||
api.GET('/albums/{id}/tracks', {
|
||||
params: {
|
||||
path: { albumId: params.id },
|
||||
path: { id: params.id },
|
||||
query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE }
|
||||
}
|
||||
}),
|
||||
@@ -50,8 +50,9 @@ export const actions: Actions = {
|
||||
const trackId = formString(form, 'trackId');
|
||||
if (!trackId) return fail(400, { message: 'Falta la canción.' });
|
||||
|
||||
const result = await api.POST('/albums/{albumId}/tracks/{trackId}', {
|
||||
params: { path: { albumId: params.id, trackId } }
|
||||
const result = await api.POST('/albums/{id}/tracks', {
|
||||
params: { path: { id: params.id } },
|
||||
body: { trackIds: [trackId] }
|
||||
});
|
||||
return failOnError(result, 'No se pudo añadir la canción.') ?? { added: true };
|
||||
}),
|
||||
@@ -60,8 +61,8 @@ export const actions: Actions = {
|
||||
const trackId = formString(form, 'trackId');
|
||||
if (!trackId) return fail(400, { message: 'Falta la canción.' });
|
||||
|
||||
const result = await api.DELETE('/albums/{albumId}/tracks/{trackId}', {
|
||||
params: { path: { albumId: params.id, trackId } }
|
||||
const result = await api.DELETE('/albums/{id}/tracks/{trackId}', {
|
||||
params: { path: { id: params.id, trackId } }
|
||||
});
|
||||
return failOnError(result, 'No se pudo quitar la canción.') ?? { removed: true };
|
||||
}),
|
||||
@@ -75,21 +76,21 @@ export const actions: Actions = {
|
||||
return fail(502, { message: cover.failMessage, detail: cover.detail });
|
||||
}
|
||||
|
||||
const result = await api.PUT('/albums/{albumId}', {
|
||||
params: { path: { albumId: params.id } },
|
||||
const result = await api.PUT('/albums/{id}', {
|
||||
params: { path: { id: params.id } },
|
||||
body: {
|
||||
newTitle: parsed.body.title,
|
||||
newDescription: parsed.body.description,
|
||||
newReleaseYear: parsed.body.releaseYear,
|
||||
newPictureIntentId: cover.intentId
|
||||
title: parsed.body.title,
|
||||
description: parsed.body.description,
|
||||
releaseYear: parsed.body.releaseYear,
|
||||
pictureIntentId: cover.intentId
|
||||
}
|
||||
});
|
||||
return failOnError(result, 'No se pudo actualizar el álbum.') ?? { edited: true };
|
||||
}),
|
||||
|
||||
delete: authedAction(async ({ api, params }) => {
|
||||
const result = await api.DELETE('/albums/{albumId}', {
|
||||
params: { path: { albumId: params.id } }
|
||||
const result = await api.DELETE('/albums/{id}', {
|
||||
params: { path: { id: params.id } }
|
||||
});
|
||||
const failure = failOnError(result, 'No se pudo eliminar el álbum.');
|
||||
if (failure) return failure;
|
||||
|
||||
@@ -46,16 +46,14 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
||||
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)
|
||||
);
|
||||
const tracks = toPage(unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.'), toTrack);
|
||||
|
||||
return {
|
||||
query,
|
||||
genre,
|
||||
genres,
|
||||
tracks,
|
||||
albums: (albumsRes?.data?.items ?? []).map((item) => toAlbum(item.album)),
|
||||
albums: (albumsRes?.data?.items ?? []).map(toAlbum),
|
||||
albumsTotal: toCount(albumsRes?.data?.totalItemCount),
|
||||
users: usersRes?.data?.items ?? [],
|
||||
usersTotal: toCount(usersRes?.data?.totalItemCount),
|
||||
|
||||
@@ -24,8 +24,8 @@ export const actions: Actions = {
|
||||
const trackId = formString(form, 'trackId');
|
||||
if (!trackId) return fail(400, { message: 'Falta la canción.' });
|
||||
|
||||
const result = await api.DELETE('/tracks/{trackId}', {
|
||||
params: { path: { trackId } }
|
||||
const result = await api.DELETE('/tracks/{id}', {
|
||||
params: { path: { id: trackId } }
|
||||
});
|
||||
return failOnError(result, 'No se pudo borrar la canción.') ?? { deleted: true };
|
||||
})
|
||||
|
||||
@@ -4,7 +4,8 @@ import { apiFor, failOnError, unwrapOrError } from '$lib/server/api';
|
||||
import { optionalUser } from '$lib/server/guards';
|
||||
import { formString } from '$lib/server/forms/fields';
|
||||
import { authedAction } from '$lib/server/actions/authedAction';
|
||||
import { PLAYLIST_TRACKS_PAGE_SIZE } from '$lib/config';
|
||||
import { PLAYLIST_TRACKS_LIMIT } from '$lib/config';
|
||||
import { fetchAllPages } from '$lib/server/pagination';
|
||||
import { toDatedTrack, toPlaylist } from '$lib/server/mappers';
|
||||
import { parsePlaylistForm } from '$lib/server/forms/playlistForm';
|
||||
import { uploadOptionalCover } from '$lib/server/upload';
|
||||
@@ -14,18 +15,21 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
const user = optionalUser(locals, url, allowAnonymousListening);
|
||||
const api = apiFor({ fetch, locals });
|
||||
|
||||
const [playlistRes, tracksRes] = await Promise.all([
|
||||
const [playlistRes, trackDtos] = await Promise.all([
|
||||
api.GET('/playlists/{id}', { params: { path: { id: params.id } } }),
|
||||
api.GET('/playlists/{playlistId}/tracks', {
|
||||
params: {
|
||||
path: { playlistId: params.id },
|
||||
query: { pageNumber: 1, pageSize: PLAYLIST_TRACKS_PAGE_SIZE }
|
||||
}
|
||||
})
|
||||
fetchAllPages(
|
||||
async (pageNumber, pageSize) =>
|
||||
(
|
||||
await api.GET('/playlists/{id}/tracks', {
|
||||
params: { path: { id: params.id }, query: { pageNumber, pageSize } }
|
||||
})
|
||||
).data,
|
||||
PLAYLIST_TRACKS_LIMIT
|
||||
)
|
||||
]);
|
||||
|
||||
const playlist = toPlaylist(unwrapOrError(playlistRes, 'Playlist no encontrada.', 404));
|
||||
const tracks = (tracksRes.data?.items ?? []).map(toDatedTrack);
|
||||
const tracks = trackDtos.map(toDatedTrack);
|
||||
|
||||
const isOwner = user !== null && String(playlist.ownerUserId) === user.sub;
|
||||
|
||||
@@ -37,8 +41,8 @@ export const actions: Actions = {
|
||||
const trackId = formString(form, 'trackId');
|
||||
if (!trackId) return fail(400, { message: 'Falta la canción.' });
|
||||
|
||||
const result = await api.DELETE('/playlists/{playlistId}/tracks/{trackId}', {
|
||||
params: { path: { playlistId: params.id, trackId } }
|
||||
const result = await api.DELETE('/playlists/{id}/tracks/{trackId}', {
|
||||
params: { path: { id: params.id, trackId } }
|
||||
});
|
||||
return failOnError(result, 'No se pudo quitar la canción.') ?? { removed: true };
|
||||
}),
|
||||
@@ -53,21 +57,21 @@ export const actions: Actions = {
|
||||
return fail(502, { message: uploaded.failMessage, detail: uploaded.detail });
|
||||
}
|
||||
|
||||
const result = await api.PUT('/playlists/{playlistId}', {
|
||||
params: { path: { playlistId: params.id } },
|
||||
const result = await api.PUT('/playlists/{id}', {
|
||||
params: { path: { id: params.id } },
|
||||
body: {
|
||||
newName: name,
|
||||
newDescription: description,
|
||||
newPictureIntentId: uploaded.intentId,
|
||||
newVisibility: visibility
|
||||
name,
|
||||
description,
|
||||
pictureIntentId: uploaded.intentId,
|
||||
visibility
|
||||
}
|
||||
});
|
||||
return failOnError(result, 'No se pudo actualizar la playlist.') ?? { edited: true };
|
||||
}),
|
||||
|
||||
delete: authedAction(async ({ api, params }) => {
|
||||
const result = await api.DELETE('/playlists/{playlistId}', {
|
||||
params: { path: { playlistId: params.id } }
|
||||
const result = await api.DELETE('/playlists/{id}', {
|
||||
params: { path: { id: params.id } }
|
||||
});
|
||||
const failure = failOnError(result, 'No se pudo eliminar la playlist.');
|
||||
if (failure) return failure;
|
||||
|
||||
@@ -15,7 +15,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
api.GET('/playlists/users/{userId}', {
|
||||
params: {
|
||||
path: { userId: params.id },
|
||||
query: { pageNumber: 1, pageSize: PROFILE_PLAYLISTS_PAGE_SIZE, onlyPublic: true }
|
||||
query: { pageNumber: 1, pageSize: PROFILE_PLAYLISTS_PAGE_SIZE }
|
||||
}
|
||||
})
|
||||
]);
|
||||
|
||||
@@ -6,9 +6,9 @@ import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
|
||||
const api = apiFor({ fetch, locals });
|
||||
const result = await api.GET('/albums/{albumId}/tracks', {
|
||||
const result = await api.GET('/albums/{id}/tracks', {
|
||||
params: {
|
||||
path: { albumId: params.id },
|
||||
path: { id: params.id },
|
||||
query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE }
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1,17 +1,26 @@
|
||||
import type { RequestHandler } from './$types';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { apiFor, unwrapOrError } from '$lib/server/api';
|
||||
import { toPage, toTrack } from '$lib/server/mappers';
|
||||
import { PLAYLIST_TRACKS_PAGE_SIZE } from '$lib/config';
|
||||
import { apiFor } from '$lib/server/api';
|
||||
import { toTrack } from '$lib/server/mappers';
|
||||
import { fetchAllPages } from '$lib/server/pagination';
|
||||
import { PLAYLIST_TRACKS_LIMIT } from '$lib/config';
|
||||
|
||||
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
|
||||
const api = apiFor({ fetch, locals });
|
||||
const result = await api.GET('/playlists/{playlistId}/tracks', {
|
||||
params: {
|
||||
path: { playlistId: params.id },
|
||||
query: { pageNumber: 1, pageSize: PLAYLIST_TRACKS_PAGE_SIZE }
|
||||
}
|
||||
});
|
||||
const tracks = await fetchAllPages(
|
||||
async (pageNumber, pageSize) =>
|
||||
(
|
||||
await api.GET('/playlists/{id}/tracks', {
|
||||
params: { path: { id: params.id }, query: { pageNumber, pageSize } }
|
||||
})
|
||||
).data,
|
||||
PLAYLIST_TRACKS_LIMIT
|
||||
);
|
||||
|
||||
return json(toPage(unwrapOrError(result, 'No se pudo cargar la playlist.'), toTrack));
|
||||
return json({
|
||||
items: tracks.map(toTrack),
|
||||
pageNumber: 1,
|
||||
hasNextPage: false,
|
||||
totalItemCount: tracks.length
|
||||
});
|
||||
};
|
||||
|
||||
@@ -18,9 +18,5 @@ export const GET: RequestHandler = async ({ url, locals, fetch }) => {
|
||||
params: { query: { name, genre, pageNumber, pageSize } }
|
||||
});
|
||||
|
||||
return json(
|
||||
toPage(unwrapOrError(result, 'No se pudieron cargar las canciones.'), (item) =>
|
||||
toTrack(item.track)
|
||||
)
|
||||
);
|
||||
return json(toPage(unwrapOrError(result, 'No se pudieron cargar las canciones.'), toTrack));
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user