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:
2026-09-24 17:30:40 +02:00
444 changed files with 14432 additions and 9275 deletions
@@ -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(
+2 -2
View File
@@ -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
});
};
+1 -5
View File
@@ -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));
};