web-player: centralize server helpers (apiFor, authedAction, form readers) and unify page sizes

This commit is contained in:
2026-09-24 05:13:51 +02:00
parent 5b2a0e4d46
commit 2915737f03
29 changed files with 293 additions and 334 deletions
@@ -1,11 +1,6 @@
import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrError
} from '$lib/server/api';
import { apiFor, authedAction, formFile, requireUser, unwrapOrError } from '$lib/server/api';
import { toAlbum, toPage } from '$lib/server/mappers';
import { ALBUMS_PAGE_SIZE } from '$lib/config';
import { parseAlbumForm } from '$lib/server/albumForm';
@@ -13,7 +8,7 @@ import { uploadPresignedImage } from '$lib/server/upload';
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await api.GET('/albums/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: ALBUMS_PAGE_SIZE } }
@@ -25,20 +20,12 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
};
export const actions: Actions = {
create: async ({ request, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
create: authedAction(async ({ api, form }) => {
const parsed = parseAlbumForm(form);
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
const cover = form.get('cover');
if (!(cover instanceof File) || cover.size === 0) {
return fail(400, { message: 'La portada es obligatoria.' });
}
const api = createApiClient({ fetch, accessToken });
const cover = formFile(form, 'cover');
if (!cover) return fail(400, { message: 'La portada es obligatoria.' });
const uploaded = await uploadPresignedImage(
(args) => api.POST('/albums/upload-picture', { body: args }),
@@ -48,12 +35,12 @@ export const actions: Actions = {
return fail(502, { message: uploaded.failMessage, detail: uploaded.detail });
}
const { data, error: err } = await api.POST('/albums', {
const { data, error } = await api.POST('/albums', {
body: { ...parsed.body, pictureIntentId: uploaded.intentId }
});
if (err || !data) return fail(502, { message: 'No se pudo crear el álbum.' });
if (error || !data) return fail(502, { message: 'No se pudo crear el álbum.' });
redirect(303, `/albums/${data.id}`);
}
})
};
@@ -1,11 +1,13 @@
import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import {
createApiClient,
apiFor,
authedAction,
failOnError,
formFile,
formString,
optionalUser,
requireAccessTokenAction,
unwrapOrError,
unwrapOrFail
unwrapOrError
} from '$lib/server/api';
import { toAlbum, toTrack } from '$lib/server/mappers';
import { ALBUM_TRACKS_PAGE_SIZE, LIBRARY_PICKER_PAGE_SIZE } from '$lib/config';
@@ -15,7 +17,7 @@ import { uploadPresignedImage } from '$lib/server/upload';
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
const { allowAnonymousListening } = await parent();
const user = optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const [albumRes, tracksRes, libraryRes] = await Promise.all([
api.GET('/albums/{id}', { params: { path: { id: params.id } } }),
@@ -49,51 +51,33 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
};
export const actions: Actions = {
addTrack: async ({ request, params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const trackId = String((await request.formData()).get('trackId') ?? '');
addTrack: authedAction(async ({ api, form, params }) => {
const trackId = formString(form, 'trackId');
if (!trackId) return fail(400, { message: 'Falta la canción.' });
const api = createApiClient({ fetch, accessToken });
const result = await api.POST('/albums/{albumId}/tracks/{trackId}', {
params: { path: { albumId: params.id, trackId } }
});
const failure = unwrapOrFail(result, 'No se pudo añadir la canción.');
if (failure) return failure;
return { added: true };
},
return failOnError(result, 'No se pudo añadir la canción.') ?? { added: true };
}),
removeTrack: async ({ request, params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const trackId = String((await request.formData()).get('trackId') ?? '');
removeTrack: authedAction(async ({ api, form, params }) => {
const trackId = formString(form, 'trackId');
if (!trackId) return fail(400, { message: 'Falta la canción.' });
const api = createApiClient({ fetch, accessToken });
const result = await api.DELETE('/albums/{albumId}/tracks/{trackId}', {
params: { path: { albumId: params.id, trackId } }
});
const failure = unwrapOrFail(result, 'No se pudo quitar la canción.');
if (failure) return failure;
return { removed: true };
},
return failOnError(result, 'No se pudo quitar la canción.') ?? { removed: true };
}),
edit: async ({ request, params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
edit: authedAction(async ({ api, form, params }) => {
const parsed = parseAlbumForm(form);
if ('failMessage' in parsed) return fail(400, { message: parsed.failMessage });
const api = createApiClient({ fetch, accessToken });
let newPictureIntentId: string | null = null;
const cover = form.get('cover');
if (cover instanceof File && cover.size > 0) {
const cover = formFile(form, 'cover');
if (cover) {
const uploaded = await uploadPresignedImage(
(args) => api.POST('/albums/upload-picture', { body: args }),
cover
@@ -113,21 +97,15 @@ export const actions: Actions = {
newPictureIntentId
}
});
const failure = unwrapOrFail(result, 'No se pudo actualizar el álbum.');
if (failure) return failure;
return { edited: true };
},
return failOnError(result, 'No se pudo actualizar el álbum.') ?? { edited: true };
}),
delete: async ({ params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const api = createApiClient({ fetch, accessToken });
delete: authedAction(async ({ api, params }) => {
const result = await api.DELETE('/albums/{albumId}', {
params: { path: { albumId: params.id } }
});
const failure = unwrapOrFail(result, 'No se pudo eliminar el álbum.');
const failure = failOnError(result, 'No se pudo eliminar el álbum.');
if (failure) return failure;
redirect(303, '/albums');
}
})
};