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');
}
})
};
@@ -1,12 +1,8 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
import { toAlbum, toPage, toTrack } from '$lib/server/mappers';
import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions';
import {
EXPLORE_ALBUMS_PAGE_SIZE,
EXPLORE_PAGE_SIZE as PAGE_SIZE,
EXPLORE_USERS_LIMIT
} from '$lib/config';
import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE, EXPLORE_USERS_LIMIT } from '$lib/config';
export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const query = url.searchParams.get('q')?.trim() ?? '';
@@ -14,7 +10,7 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
const user = locals.user;
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const genresRes = await api.GET('/genres');
const genres = genresRes.data ?? [];
@@ -26,7 +22,7 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
name: query || undefined,
genre: genre ?? undefined,
pageNumber: page,
pageSize: PAGE_SIZE
pageSize: EXPLORE_PAGE_SIZE
}
}
});
@@ -2,20 +2,20 @@ import type { PageServerLoad, Actions } from './$types';
import { fail } from '@sveltejs/kit';
import { toDatedTrack, toPage } from '$lib/server/mappers';
import {
createApiClient,
requireAccessTokenAction,
apiFor,
authedAction,
failOnError,
formString,
requireUser,
unwrapOrError,
unwrapOrFail
unwrapOrError
} from '$lib/server/api';
const PAGE_SIZE = 50;
import { LIBRARY_PAGE_SIZE } from '$lib/config';
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('/tracks/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: LIBRARY_PAGE_SIZE } }
});
return {
@@ -24,18 +24,13 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
};
export const actions: Actions = {
deleteTrack: async ({ request, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const trackId = String((await request.formData()).get('trackId') ?? '');
deleteTrack: authedAction(async ({ api, form }) => {
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('/tracks/{trackId}', {
params: { path: { trackId } }
});
const failure = unwrapOrFail(result, 'No se pudo borrar la canción.');
if (failure) return failure;
return { deleted: true };
}
return failOnError(result, 'No se pudo borrar la canción.') ?? { deleted: true };
})
};
@@ -1,11 +1,11 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api';
import { apiFor, requireUser, unwrapOrError } from '$lib/server/api';
import { toMix } from '$lib/server/mappers';
import { addTrackAction } from '$lib/server/playlistActions';
export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const mixRes = await api.GET('/mixes/{id}', { params: { path: { id: params.id } } });
@@ -1,11 +1,14 @@
import type { PageServerLoad, Actions } from './$types';
import { fail, redirect } from '@sveltejs/kit';
import {
createApiClient,
requireAccessTokenAction,
apiFor,
authedAction,
formFile,
formString,
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 { uploadPresignedImage } from '$lib/server/upload';
@@ -13,9 +16,9 @@ const MAX_NAME = 100;
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('/playlists/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PLAYLISTS_PAGE_SIZE } }
});
const data = unwrapOrError(result, 'No se pudieron cargar tus playlists.');
@@ -24,7 +27,10 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
data.items.map((playlist) =>
api
.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 500 } }
params: {
path: { playlistId: playlist.id },
query: { pageNumber: 1, pageSize: PLAYLIST_SUMMARY_TRACKS_PAGE_SIZE }
}
})
.then((res) => (res.data?.items ?? []).map(toTrack))
)
@@ -44,15 +50,10 @@ 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();
const name = String(form.get('name') ?? '').trim();
const description = String(form.get('description') ?? '').trim();
const visibility = form.get('visibility') === 'public' ? 'Public' : 'Private';
const cover = form.get('cover');
create: authedAction(async ({ api, form }) => {
const name = formString(form, 'name').trim();
const description = formString(form, 'description').trim();
const visibility = formString(form, 'visibility') === 'public' ? 'Public' : 'Private';
if (name === '' || name.length > MAX_NAME) {
return fail(400, {
@@ -62,10 +63,9 @@ export const actions: Actions = {
});
}
const api = createApiClient({ fetch, accessToken });
let pictureIntentId: string | null = null;
if (cover instanceof File && cover.size > 0) {
const cover = formFile(form, 'cover');
if (cover) {
const result = await uploadPresignedImage(
(args) => api.POST('/playlists/upload-picture', { body: args }),
cover
@@ -76,13 +76,13 @@ export const actions: Actions = {
pictureIntentId = result.intentId;
}
const { data, error: err } = await api.POST('/playlists', {
const { data, error } = await api.POST('/playlists', {
body: { name, description, pictureIntentId, visibility }
});
if (err || !data)
if (error || !data)
return fail(502, { message: 'No se pudo crear la playlist.', name, description });
redirect(303, `/playlists/${data.id}`);
}
})
};
@@ -1,12 +1,15 @@
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 { PLAYLIST_TRACKS_PAGE_SIZE } from '$lib/config';
import { toDatedTrack, toPlaylist } from '$lib/server/mappers';
import { uploadPresignedImage } from '$lib/server/upload';
@@ -15,12 +18,15 @@ const MAX_NAME = 100;
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 [playlistRes, tracksRes] = 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: 200 } }
params: {
path: { playlistId: params.id },
query: { pageNumber: 1, pageSize: PLAYLIST_TRACKS_PAGE_SIZE }
}
})
]);
@@ -33,37 +39,27 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
};
export const actions: Actions = {
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('/playlists/{playlistId}/tracks/{trackId}', {
params: { path: { playlistId: 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 };
}),
rename: async ({ request, params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
const name = String(form.get('name') ?? '').trim();
const description = String(form.get('description') ?? '').trim();
const newVisibility = form.get('visibility') === 'public' ? 'Public' : 'Private';
const cover = form.get('cover');
edit: authedAction(async ({ api, form, params }) => {
const name = formString(form, 'name').trim();
const description = formString(form, 'description').trim();
const newVisibility = formString(form, 'visibility') === 'public' ? 'Public' : 'Private';
if (name === '' || name.length > MAX_NAME) {
return fail(400, { message: 'El nombre es obligatorio (máx. 100 caracteres).' });
}
const api = createApiClient({ fetch, accessToken });
let newPictureIntentId: string | null = null;
if (cover instanceof File && cover.size > 0) {
const cover = formFile(form, 'cover');
if (cover) {
const result = await uploadPresignedImage(
(args) => api.POST('/playlists/upload-picture', { body: args }),
cover
@@ -78,21 +74,15 @@ export const actions: Actions = {
params: { path: { playlistId: params.id } },
body: { newName: name, newDescription: description, newPictureIntentId, newVisibility }
});
const failure = unwrapOrFail(result, 'No se pudo actualizar la playlist.');
if (failure) return failure;
return { renamed: true };
},
return failOnError(result, 'No se pudo actualizar la playlist.') ?? { 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('/playlists/{playlistId}', {
params: { path: { playlistId: params.id } }
});
const failure = unwrapOrFail(result, 'No se pudo eliminar la playlist.');
const failure = failOnError(result, 'No se pudo eliminar la playlist.');
if (failure) return failure;
redirect(303, '/playlists');
}
})
};
@@ -106,7 +106,7 @@
<Modal open={editing} onClose={() => (editing = false)} title="Editar playlist">
<PlaylistForm
action="?/rename"
action="?/edit"
initialName={playlist.name}
initialDescription={playlist.description ?? ''}
initialVisibility={playlist.visibility === 'Public' ? 'public' : 'private'}
@@ -1,10 +1,12 @@
import type { Actions, PageServerLoad } from './$types';
import { fail } from '@sveltejs/kit';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrFail
apiFor,
authedAction,
formFile,
formString,
requireData,
requireUser
} from '$lib/server/api';
import { putPresigned, extOf, contentTypeOf, AUDIO_TYPES, IMAGE_TYPES } from '$lib/server/upload';
import { findConflict, genreInfo } from '$lib/data/genres';
@@ -16,37 +18,28 @@ const MAX_TITLE = 100;
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const { data } = await api.GET('/genres/available');
return { genres: data ?? [] };
};
export const actions: Actions = {
default: async ({ request, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals, 'Inicia sesión para subir música.');
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
const title = String(form.get('title') ?? '').trim();
const audio = form.get('audio');
const cover = form.get('cover');
default: authedAction(async ({ api, form }) => {
const title = formString(form, 'title').trim();
const audio = formFile(form, 'audio');
const cover = formFile(form, 'cover');
const rawTags = form.getAll('tags').map(String);
const isExplicit = form.get('isExplicit') === 'on';
if (title === '' || title.length > MAX_TITLE) {
return fail(400, { message: 'El título es obligatorio (máx. 100 caracteres).' });
}
if (!(audio instanceof File) || audio.size === 0) {
return fail(400, { message: 'Selecciona un archivo de audio.' });
}
if (!(cover instanceof File) || cover.size === 0) {
return fail(400, { message: 'Selecciona una portada.' });
}
if (!audio) return fail(400, { message: 'Selecciona un archivo de audio.' });
if (!cover) return fail(400, { message: 'Selecciona una portada.' });
if (rawTags.length === 0) {
return fail(400, { message: 'Elige al menos un género.' });
}
const api = createApiClient({ fetch, accessToken });
const { data: available } = await api.GET('/genres/available');
const options = available ?? [];
const known = new Set<string>(options.map((option) => option.genre));
@@ -75,9 +68,11 @@ export const actions: Actions = {
}
});
const urlsFailure = unwrapOrFail(urlsResult, 'No se pudieron reservar las URLs de subida.');
const { data: urls, failure: urlsFailure } = requireData(
urlsResult,
'No se pudieron reservar las URLs de subida.'
);
if (urlsFailure) return urlsFailure;
const urls = urlsResult.data!;
try {
const [audioBuffer, coverBuffer] = await Promise.all([
@@ -105,10 +100,12 @@ export const actions: Actions = {
}
});
const trackFailure = unwrapOrFail(trackResult, 'No se pudo crear la pista tras la subida.');
const { data: track, failure: trackFailure } = requireData(
trackResult,
'No se pudo crear la pista tras la subida.'
);
if (trackFailure) return trackFailure;
const track = trackResult.data!;
return { success: true, trackId: track.id, title: track.title };
}
}, 'Inicia sesión para subir música.')
};
@@ -1,14 +1,13 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api';
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 { followUserAction, unfollowUserAction } from '$lib/server/followActions';
const PROFILE_PLAYLISTS_PAGE_SIZE = 50;
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
const { allowAnonymousListening } = await parent();
const viewer = optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const [profileRes, playlistsRes] = await Promise.all([
api.GET('/users/{id}/profile', { params: { path: { id: params.id } } }),
@@ -27,7 +26,10 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
playlistItems.map((playlist) =>
api
.GET('/playlists/{playlistId}/tracks', {
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 1 } }
params: {
path: { playlistId: playlist.id },
query: { pageNumber: 1, pageSize: COUNT_ONLY_PAGE_SIZE }
}
})
.then((res) => Number(res.data?.totalItemCount ?? 0))
)
@@ -1,5 +1,5 @@
import type { PageServerLoad } from './$types';
import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api';
import { apiFor, optionalUser, unwrapOrError } from '$lib/server/api';
import { toPage } from '$lib/server/mappers';
import { fetchFollowList } from '$lib/server/follows';
import { FOLLOW_LIST_PAGE_SIZE } from '$lib/config';
@@ -7,7 +7,7 @@ import { FOLLOW_LIST_PAGE_SIZE } from '$lib/config';
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
const { allowAnonymousListening } = await parent();
const viewer = optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const [profileRes, listRes] = await Promise.all([
api.GET('/users/{id}/profile', { params: { path: { id: params.id } } }),
+35 -51
View File
@@ -1,65 +1,21 @@
import { redirect } from '@sveltejs/kit';
import type { LayoutServerLoad } from './$types';
import { getAllowAnonymousListening } from '$lib/server/playbackConfig';
import { createApiClient } from '$lib/server/api';
import { apiFor, loginRedirect } from '$lib/server/api';
import { 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';
const PUBLIC_PATHS = new Set(['/auth']);
async function fetchUserPlaylists(
fetchFn: typeof fetch,
accessToken: string | null,
userId: string
) {
try {
const api = createApiClient({ fetch: fetchFn, accessToken: accessToken ?? undefined });
const { data } = await api.GET('/playlists/users/{userId}', {
params: {
path: { userId },
query: { pageNumber: 1, pageSize: PLAYLIST_PICKER_PAGE_SIZE }
}
});
return { items: (data?.items ?? []).map(toPlaylist), total: Number(data?.totalItemCount ?? 0) };
} catch {
return { items: [], total: 0 };
}
}
async function fetchLikedTracks(fetchFn: typeof fetch, accessToken: string | null) {
try {
const api = createApiClient({ fetch: fetchFn, accessToken: accessToken ?? undefined });
const { data } = await api.GET('/likes', {
params: { query: { pageNumber: 1, pageSize: LIKED_TRACKS_PAGE_SIZE } }
});
return (data?.items ?? []).map(toLikedTrack);
} catch {
return [];
}
}
async function fetchLastPlayedTrack(
fetchFn: typeof fetch,
accessToken: string | null,
userId: string
) {
try {
const api = createApiClient({ fetch: fetchFn, accessToken: accessToken ?? undefined });
const { data } = await api.GET('/users/{id}/last-listened-track', {
params: { path: { id: userId } }
});
return data ? toTrack(data) : null;
} catch {
return null;
}
function orFallback<T>(load: Promise<T>, fallback: T): Promise<T> {
return load.catch(() => fallback);
}
export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
const allowAnonymousListening = await getAllowAnonymousListening(fetch);
if (!locals.user && !allowAnonymousListening && !PUBLIC_PATHS.has(url.pathname)) {
redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
loginRedirect(url);
}
const accountUrl = `${authConfig.issuer.replace(/\/+$/, '')}/ui/console`;
@@ -76,10 +32,38 @@ export const load: LayoutServerLoad = async ({ locals, url, fetch }) => {
};
}
const api = apiFor({ fetch, locals });
const userId = locals.user.sub;
const [userPlaylists, likedTracks, lastPlayedTrack] = await Promise.all([
fetchUserPlaylists(fetch, locals.accessToken, locals.user.sub),
fetchLikedTracks(fetch, locals.accessToken),
fetchLastPlayedTrack(fetch, locals.accessToken, locals.user.sub)
orFallback(
api
.GET('/playlists/users/{userId}', {
params: {
path: { userId },
query: { pageNumber: 1, pageSize: PLAYLIST_PICKER_PAGE_SIZE }
}
})
.then(({ data }) => ({
items: (data?.items ?? []).map(toPlaylist),
total: Number(data?.totalItemCount ?? 0)
})),
{ items: [], total: 0 }
),
orFallback(
api
.GET('/likes', {
params: { query: { pageNumber: 1, pageSize: LIKED_TRACKS_PAGE_SIZE } }
})
.then(({ data }) => (data?.items ?? []).map(toLikedTrack)),
[]
),
orFallback(
api
.GET('/users/{id}/last-listened-track', { params: { path: { id: userId } } })
.then(({ data }) => (data ? toTrack(data) : null)),
null
)
]);
return {
+2 -2
View File
@@ -1,6 +1,6 @@
import { redirect } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, requireUser } from '$lib/server/api';
import { apiFor, requireUser } from '$lib/server/api';
import { toMix, toPlaylist, toTrack } from '$lib/server/mappers';
import { addTrackAction } from '$lib/server/playlistActions';
import { HOME_MIXES_LIMIT, HOME_SHELF_LIMIT, HOME_SPOTLIGHT_TRACKS_LIMIT } from '$lib/config';
@@ -9,7 +9,7 @@ import { pickGreeting } from '$lib/server/greeting';
export const load: PageServerLoad = async ({ locals, url, fetch }) => {
if (!locals.user) redirect(302, '/explore');
const user = requireUser(locals, url);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const playlistsPromise = api.GET('/playlists/users/{userId}', {
params: {
@@ -1,11 +1,11 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
import { toPage, toTrack } from '$lib/server/mappers';
import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config';
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await api.GET('/albums/{albumId}/tracks', {
params: {
path: { albumId: params.id },
+2 -2
View File
@@ -1,6 +1,6 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient } from '$lib/server/api';
import { apiFor } from '$lib/server/api';
export const POST: RequestHandler = async ({ request, locals, fetch }) => {
if (!locals.accessToken) return json({ message: 'Inicia sesión.' }, { status: 401 });
@@ -10,7 +10,7 @@ export const POST: RequestHandler = async ({ request, locals, fetch }) => {
if (typeof trackId !== 'string' || trackId === '')
return json({ message: 'Canción inválida.' }, { status: 400 });
const api = createApiClient({ fetch, accessToken: locals.accessToken });
const api = apiFor({ fetch, locals });
const result = await api.POST('/likes/toggle', { body: { trackId } });
if (result.error || result.data === undefined)
@@ -1,6 +1,6 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient } from '$lib/server/api';
import { apiFor } from '$lib/server/api';
export const PUT: RequestHandler = async ({ params, request, locals, fetch }) => {
if (!locals.accessToken) return json({ message: 'Inicia sesión.' }, { status: 401 });
@@ -10,7 +10,7 @@ export const PUT: RequestHandler = async ({ params, request, locals, fetch }) =>
if (typeof playedSeconds !== 'number' || !Number.isFinite(playedSeconds) || playedSeconds < 0)
return json({ message: 'Tiempo inválido.' }, { status: 400 });
const api = createApiClient({ fetch, accessToken: locals.accessToken });
const api = apiFor({ fetch, locals });
const result = await api.PUT('/tracks/listens/{listenId}/progress', {
params: { path: { listenId: params.id } },
body: { playedSeconds }
@@ -1,11 +1,11 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
import { toPage, toTrack } from '$lib/server/mappers';
import { PLAYLIST_TRACKS_PAGE_SIZE } from '$lib/config';
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await api.GET('/playlists/{playlistId}/tracks', {
params: {
path: { playlistId: params.id },
+2 -2
View File
@@ -1,6 +1,6 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
import { toPage, toTrack } from '$lib/server/mappers';
import { EXPLORE_PAGE_SIZE, MAX_PAGE_SIZE } from '$lib/config';
@@ -13,7 +13,7 @@ export const GET: RequestHandler = async ({ url, locals, fetch }) => {
Math.max(1, Number(url.searchParams.get('pageSize')) || EXPLORE_PAGE_SIZE)
);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await api.GET('/tracks', {
params: { query: { name, genre, pageNumber, pageSize } }
});
@@ -1,9 +1,9 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
export const GET: RequestHandler = async ({ params, locals, fetch }) => {
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await api.GET('/tracks/{id}/stream', {
params: { path: { id: params.id } }
});
@@ -1,13 +1,13 @@
import type { RequestHandler } from './$types';
import { json } from '@sveltejs/kit';
import { createApiClient, unwrapOrError } from '$lib/server/api';
import { apiFor, unwrapOrError } from '$lib/server/api';
import { toPage } from '$lib/server/mappers';
import { fetchFollowList } from '$lib/server/follows';
import { FOLLOW_LIST_PAGE_SIZE } from '$lib/config';
export const GET: RequestHandler = async ({ params, url, locals, fetch }) => {
const pageNumber = Math.max(1, Number(url.searchParams.get('pageNumber')) || 1);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const api = apiFor({ fetch, locals });
const result = await fetchFollowList(
api,
params.list,
@@ -34,8 +34,8 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
expectedNonce: nonce,
idTokenExpected: true
});
} catch (exception) {
console.error('Token exchange failed', exception);
} catch (err) {
console.error('Token exchange failed', err);
error(401, 'La autenticación ha fallado.');
}