diff --git a/web-player/src/lib/config.ts b/web-player/src/lib/config.ts index 7701d3f..af3366e 100644 --- a/web-player/src/lib/config.ts +++ b/web-player/src/lib/config.ts @@ -8,6 +8,11 @@ export const ALBUM_TRACKS_PAGE_SIZE = 200; export const PLAYLIST_TRACKS_PAGE_SIZE = 200; export const ALBUM_EARLIEST_YEAR = 1877; export const LIBRARY_PICKER_PAGE_SIZE = 50; +export const LIBRARY_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 COUNT_ONLY_PAGE_SIZE = 1; export const EXPLORE_ALBUMS_PAGE_SIZE = 10; export const EXPLORE_ALBUMS_LIMIT = 18; export const HOME_MIXES_LIMIT = 6; diff --git a/web-player/src/lib/player/player.svelte.ts b/web-player/src/lib/player/player.svelte.ts index 8b73775..d0c856d 100644 --- a/web-player/src/lib/player/player.svelte.ts +++ b/web-player/src/lib/player/player.svelte.ts @@ -242,17 +242,13 @@ class PlayerState { await audio.play(); this.loading = false; if (track) this.#pushRecent(track); - } catch (exception) { - console.error('playback failed', exception); + } catch (err) { + console.error('playback failed', err); if (token !== this.#loadToken) return; this.loading = false; this.playing = false; - if (exception instanceof DOMException && exception.name === 'AbortError') return; - this.#fail( - exception instanceof Error && !(exception instanceof DOMException) - ? exception.message - : undefined - ); + if (err instanceof DOMException && err.name === 'AbortError') return; + this.#fail(err instanceof Error && !(err instanceof DOMException) ? err.message : undefined); } } diff --git a/web-player/src/lib/server/albumForm.ts b/web-player/src/lib/server/albumForm.ts index bed1fa4..e3dfdf0 100644 --- a/web-player/src/lib/server/albumForm.ts +++ b/web-player/src/lib/server/albumForm.ts @@ -1,3 +1,4 @@ +import { formString } from '$lib/server/api'; import { ALBUM_EARLIEST_YEAR } from '$lib/config'; interface AlbumFormBody { @@ -7,17 +8,17 @@ interface AlbumFormBody { } export function parseAlbumForm(form: FormData): { body: AlbumFormBody } | { failMessage: string } { - const title = String(form.get('title') ?? '').trim(); + const title = formString(form, 'title').trim(); if (title === '' || title.length > 200) { return { failMessage: 'El título es obligatorio (máx. 200 caracteres).' }; } - const description = String(form.get('description') ?? '').trim(); + const description = formString(form, 'description').trim(); if (description.length > 256) { return { failMessage: 'La descripción no puede pasar de 256 caracteres.' }; } - const rawYear = String(form.get('releaseYear') ?? '').trim(); + const rawYear = formString(form, 'releaseYear').trim(); const latestYear = new Date().getFullYear() + 1; let releaseYear: number | null = null; diff --git a/web-player/src/lib/server/api.ts b/web-player/src/lib/server/api.ts index 722a5c7..fa2f878 100644 --- a/web-player/src/lib/server/api.ts +++ b/web-player/src/lib/server/api.ts @@ -1,5 +1,5 @@ import createClient from 'openapi-fetch'; -import { error, fail, redirect, type ActionFailure } from '@sveltejs/kit'; +import { error, fail, redirect, type ActionFailure, type RequestEvent } from '@sveltejs/kit'; import type { paths } from '$lib/api/schema'; import type { SessionUser } from '$lib/types'; import { apiConfig } from '$lib/server/config'; @@ -17,9 +17,18 @@ export function createApiClient({ fetch, accessToken }: ApiClientOptions) { }); } +export type ApiClient = ReturnType; + +export function apiFor({ fetch, locals }: Pick): ApiClient { + return createApiClient({ fetch, accessToken: locals.accessToken ?? undefined }); +} + +export function loginRedirect(url: URL): never { + redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`); +} + export function requireUser(locals: App.Locals, url: URL): SessionUser { - if (!locals.user) - redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`); + if (!locals.user) loginRedirect(url); return locals.user; } @@ -29,16 +38,42 @@ export function optionalUser( allowAnonymousListening: boolean ): SessionUser | null { if (locals.user) return locals.user; - if (!allowAnonymousListening) - redirect(302, `/auth?returnTo=${encodeURIComponent(url.pathname + url.search)}`); + if (!allowAnonymousListening) loginRedirect(url); return null; } -export function requireAccessTokenAction( - locals: App.Locals, - message = 'Inicia sesión.' -): string | ActionFailure<{ message: string }> { - return locals.accessToken ?? fail(401, { message }); +export function formString(form: FormData, key: string): string { + return String(form.get(key) ?? ''); +} + +export function formFile(form: FormData, key: string): File | null { + const value = form.get(key); + return value instanceof File && value.size > 0 ? value : null; +} + +interface ActionContext { + api: ApiClient; + form: FormData; + params: Record; + locals: App.Locals; + url: URL; +} + +export function authedAction( + handler: (context: ActionContext) => R | Promise, + unauthorizedMessage = 'Inicia sesión.' +) { + return async (event: RequestEvent) => { + if (!event.locals.accessToken) return fail(401, { message: unauthorizedMessage }); + const form = await event.request.formData(); + return handler({ + api: apiFor(event), + form, + params: event.params as Record, + locals: event.locals, + url: event.url + }); + }; } export function unwrapOrError( @@ -70,9 +105,18 @@ export function apiErrorDetail(error: unknown): string | undefined { return undefined; } -export function unwrapOrFail( - result: { error?: unknown }, - message: string -): ActionFailure<{ message: string; detail?: string }> | undefined { +type Failure = ActionFailure<{ message: string; detail?: string }>; + +export function failOnError(result: { error?: unknown }, message: string): Failure | undefined { return result.error ? fail(502, { message, detail: apiErrorDetail(result.error) }) : undefined; } + +export function requireData( + result: { data?: T; error?: unknown }, + message: string +): { data: T; failure?: undefined } | { data?: undefined; failure: Failure } { + if (result.error || result.data === undefined) { + return { failure: fail(502, { message, detail: apiErrorDetail(result.error) }) }; + } + return { data: result.data }; +} diff --git a/web-player/src/lib/server/auth.ts b/web-player/src/lib/server/auth.ts index b17178e..cce923c 100644 --- a/web-player/src/lib/server/auth.ts +++ b/web-player/src/lib/server/auth.ts @@ -36,9 +36,9 @@ export function getOidcConfig(): Promise { return secret ? client.discovery(issuer, authConfig.clientId, secret, undefined, options) : client.discovery(issuer, authConfig.clientId, undefined, client.None(), options); - })().catch((exception) => { + })().catch((err) => { configPromise = undefined; - throw exception; + throw err; }); return configPromise; } @@ -151,8 +151,8 @@ export async function refreshSession(session: Session): Promise idToken: tokens.id_token ?? session.idToken, expiresAt: Math.floor(Date.now() / 1000) + (tokens.expires_in ?? 3600) }; - } catch (exception) { - console.error('Token refresh failed', exception); + } catch (err) { + console.error('Token refresh failed', err); return null; } } diff --git a/web-player/src/lib/server/followActions.ts b/web-player/src/lib/server/followActions.ts index 9a7e8df..93b81b3 100644 --- a/web-player/src/lib/server/followActions.ts +++ b/web-player/src/lib/server/followActions.ts @@ -1,28 +1,24 @@ -import { fail, type RequestEvent } from '@sveltejs/kit'; -import { createApiClient, requireAccessTokenAction } from '$lib/server/api'; +import { fail } from '@sveltejs/kit'; +import { authedAction } from '$lib/server/api'; -async function setFollowing({ request, params, locals, fetch }: RequestEvent, follow: boolean) { - const accessToken = requireAccessTokenAction(locals); - if (typeof accessToken !== 'string') return accessToken; +const setFollowing = (follow: boolean) => + authedAction(async ({ api, params }) => { + const id = params.id ?? ''; + if (!id) return fail(400, { message: 'Falta el usuario.' }); - const form = await request.formData(); - const id = String(form.get('userId') ?? '') || String(params.id ?? ''); - if (!id) return fail(400, { message: 'Falta el usuario.' }); + const options = { params: { path: { id } } }; + const { error } = follow + ? await api.POST('/users/{id}/follow', options) + : await api.DELETE('/users/{id}/follow', options); - const api = createApiClient({ fetch, accessToken }); - const options = { params: { path: { id } } }; - const { error } = follow - ? await api.POST('/users/{id}/follow', options) - : await api.DELETE('/users/{id}/follow', options); + if (error) { + return fail(502, { + message: follow ? 'No se pudo seguir al usuario.' : 'No se pudo dejar de seguir al usuario.' + }); + } - if (error) { - return fail(502, { - message: follow ? 'No se pudo seguir al usuario.' : 'No se pudo dejar de seguir al usuario.' - }); - } + return { following: follow, userId: id }; + }); - return { following: follow, userId: id }; -} - -export const followUserAction = (event: RequestEvent) => setFollowing(event, true); -export const unfollowUserAction = (event: RequestEvent) => setFollowing(event, false); +export const followUserAction = setFollowing(true); +export const unfollowUserAction = setFollowing(false); diff --git a/web-player/src/lib/server/follows.ts b/web-player/src/lib/server/follows.ts index c51719c..0a8eb50 100644 --- a/web-player/src/lib/server/follows.ts +++ b/web-player/src/lib/server/follows.ts @@ -1,8 +1,6 @@ -import type { createApiClient } from '$lib/server/api'; +import type { ApiClient } from '$lib/server/api'; import type { FollowList } from '$lib/types'; -type ApiClient = ReturnType; - export function fetchFollowList( api: ApiClient, list: FollowList, diff --git a/web-player/src/lib/server/playlistActions.ts b/web-player/src/lib/server/playlistActions.ts index 318b4ef..9213f5b 100644 --- a/web-player/src/lib/server/playlistActions.ts +++ b/web-player/src/lib/server/playlistActions.ts @@ -1,26 +1,21 @@ -import { fail, type RequestEvent } from '@sveltejs/kit'; -import { createApiClient, requireAccessTokenAction } from '$lib/server/api'; +import { fail } from '@sveltejs/kit'; +import { authedAction, formString } from '$lib/server/api'; import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config'; -export async function addTrackAction({ request, params, locals, fetch }: RequestEvent) { - const accessToken = requireAccessTokenAction(locals); - if (typeof accessToken !== 'string') return accessToken; - - const form = await request.formData(); - const playlistId = String(form.get('playlistId') ?? '') || String(params.id ?? ''); - const trackId = String(form.get('trackId') ?? ''); +export const addTrackAction = authedAction(async ({ api, form, params }) => { + const playlistId = formString(form, 'playlistId') || (params.id ?? ''); + const trackId = formString(form, 'trackId'); if (!playlistId || !trackId) { return fail(400, { message: 'Faltan datos de la canción.' }); } - const api = createApiClient({ fetch, accessToken }); - const { error: err, response } = await api.POST('/playlists/{playlistId}/tracks', { + const { error, response } = await api.POST('/playlists/{playlistId}/tracks', { params: { path: { playlistId } }, body: { trackId } }); - if (err) { + if (error) { if (response?.status === 409) { return fail(409, { message: 'La canción ya está en esa playlist.', trackId }); } @@ -28,37 +23,32 @@ export async function addTrackAction({ request, params, locals, fetch }: Request } return { added: true, trackId, playlistId }; -} +}); -export async function addAlbumToPlaylistAction({ request, params, locals, fetch }: RequestEvent) { - const accessToken = requireAccessTokenAction(locals); - if (typeof accessToken !== 'string') return accessToken; - - const form = await request.formData(); - const playlistId = String(form.get('playlistId') ?? '') || String(params.id ?? ''); - const albumId = String(form.get('albumId') ?? ''); +export const addAlbumToPlaylistAction = authedAction(async ({ api, form, params }) => { + const playlistId = formString(form, 'playlistId') || (params.id ?? ''); + const albumId = formString(form, 'albumId'); if (!playlistId || !albumId) { return fail(400, { message: 'Faltan datos del álbum.' }); } - const api = createApiClient({ fetch, accessToken }); - const { data: tracks, error: tracksErr } = await api.GET('/albums/{albumId}/tracks', { + const { data: tracks, error: tracksError } = await api.GET('/albums/{albumId}/tracks', { params: { path: { albumId }, query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } } }); - if (tracksErr || !tracks) { + if (tracksError || !tracks) { return fail(502, { message: 'No se pudo cargar el álbum.', albumId }); } let added = 0; for (const track of tracks.items) { - const { error: err } = await api.POST('/playlists/{playlistId}/tracks', { + const { error } = await api.POST('/playlists/{playlistId}/tracks', { params: { path: { playlistId } }, - body: { trackId: String(track.id) } + body: { trackId: track.id } }); - if (!err) added++; + if (!error) added++; } return { addedAlbum: true, albumId, playlistId, added }; -} +}); diff --git a/web-player/src/routes/(app)/albums/+page.server.ts b/web-player/src/routes/(app)/albums/+page.server.ts index 9855829..692b1e3 100644 --- a/web-player/src/routes/(app)/albums/+page.server.ts +++ b/web-player/src/routes/(app)/albums/+page.server.ts @@ -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}`); - } + }) }; diff --git a/web-player/src/routes/(app)/albums/[id]/+page.server.ts b/web-player/src/routes/(app)/albums/[id]/+page.server.ts index 0e07eb2..8b494e6 100644 --- a/web-player/src/routes/(app)/albums/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/albums/[id]/+page.server.ts @@ -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'); - } + }) }; diff --git a/web-player/src/routes/(app)/explore/+page.server.ts b/web-player/src/routes/(app)/explore/+page.server.ts index 9fc7f80..05d1de5 100644 --- a/web-player/src/routes/(app)/explore/+page.server.ts +++ b/web-player/src/routes/(app)/explore/+page.server.ts @@ -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 } } }); diff --git a/web-player/src/routes/(app)/library/+page.server.ts b/web-player/src/routes/(app)/library/+page.server.ts index 4550fd7..d3eeaa7 100644 --- a/web-player/src/routes/(app)/library/+page.server.ts +++ b/web-player/src/routes/(app)/library/+page.server.ts @@ -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 }; + }) }; diff --git a/web-player/src/routes/(app)/mixes/[id]/+page.server.ts b/web-player/src/routes/(app)/mixes/[id]/+page.server.ts index 59bb8b2..c9e0518 100644 --- a/web-player/src/routes/(app)/mixes/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/mixes/[id]/+page.server.ts @@ -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 } } }); diff --git a/web-player/src/routes/(app)/playlists/+page.server.ts b/web-player/src/routes/(app)/playlists/+page.server.ts index 3502758..54c9f25 100644 --- a/web-player/src/routes/(app)/playlists/+page.server.ts +++ b/web-player/src/routes/(app)/playlists/+page.server.ts @@ -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}`); - } + }) }; diff --git a/web-player/src/routes/(app)/playlists/[id]/+page.server.ts b/web-player/src/routes/(app)/playlists/[id]/+page.server.ts index 1cdedfd..797d354 100644 --- a/web-player/src/routes/(app)/playlists/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/playlists/[id]/+page.server.ts @@ -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'); - } + }) }; diff --git a/web-player/src/routes/(app)/playlists/[id]/+page.svelte b/web-player/src/routes/(app)/playlists/[id]/+page.svelte index 3685cdd..a485dd4 100644 --- a/web-player/src/routes/(app)/playlists/[id]/+page.svelte +++ b/web-player/src/routes/(app)/playlists/[id]/+page.svelte @@ -106,7 +106,7 @@ (editing = false)} title="Editar playlist"> { 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(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.') }; diff --git a/web-player/src/routes/(app)/user/[id]/+page.server.ts b/web-player/src/routes/(app)/user/[id]/+page.server.ts index 5e278c8..4133a25 100644 --- a/web-player/src/routes/(app)/user/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/user/[id]/+page.server.ts @@ -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)) ) diff --git a/web-player/src/routes/(app)/user/[id]/[list=followList]/+page.server.ts b/web-player/src/routes/(app)/user/[id]/[list=followList]/+page.server.ts index 1af94c9..d7befb2 100644 --- a/web-player/src/routes/(app)/user/[id]/[list=followList]/+page.server.ts +++ b/web-player/src/routes/(app)/user/[id]/[list=followList]/+page.server.ts @@ -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 } } }), diff --git a/web-player/src/routes/+layout.server.ts b/web-player/src/routes/+layout.server.ts index 26879de..dcb70b2 100644 --- a/web-player/src/routes/+layout.server.ts +++ b/web-player/src/routes/+layout.server.ts @@ -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(load: Promise, fallback: T): Promise { + 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 { diff --git a/web-player/src/routes/+page.server.ts b/web-player/src/routes/+page.server.ts index 98d5a0f..ed82526 100644 --- a/web-player/src/routes/+page.server.ts +++ b/web-player/src/routes/+page.server.ts @@ -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: { diff --git a/web-player/src/routes/api/albums/[id]/tracks/+server.ts b/web-player/src/routes/api/albums/[id]/tracks/+server.ts index 8df1853..853006d 100644 --- a/web-player/src/routes/api/albums/[id]/tracks/+server.ts +++ b/web-player/src/routes/api/albums/[id]/tracks/+server.ts @@ -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 }, diff --git a/web-player/src/routes/api/likes/+server.ts b/web-player/src/routes/api/likes/+server.ts index 836c30c..bb2659c 100644 --- a/web-player/src/routes/api/likes/+server.ts +++ b/web-player/src/routes/api/likes/+server.ts @@ -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) diff --git a/web-player/src/routes/api/listens/[id]/progress/+server.ts b/web-player/src/routes/api/listens/[id]/progress/+server.ts index 7cf9e67..fc83bad 100644 --- a/web-player/src/routes/api/listens/[id]/progress/+server.ts +++ b/web-player/src/routes/api/listens/[id]/progress/+server.ts @@ -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 } diff --git a/web-player/src/routes/api/playlists/[id]/tracks/+server.ts b/web-player/src/routes/api/playlists/[id]/tracks/+server.ts index 29b103d..666e35f 100644 --- a/web-player/src/routes/api/playlists/[id]/tracks/+server.ts +++ b/web-player/src/routes/api/playlists/[id]/tracks/+server.ts @@ -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 }, diff --git a/web-player/src/routes/api/tracks/+server.ts b/web-player/src/routes/api/tracks/+server.ts index 6cd7c1d..f90eeed 100644 --- a/web-player/src/routes/api/tracks/+server.ts +++ b/web-player/src/routes/api/tracks/+server.ts @@ -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 } } }); diff --git a/web-player/src/routes/api/tracks/[id]/stream/+server.ts b/web-player/src/routes/api/tracks/[id]/stream/+server.ts index 84ec1bd..b219b20 100644 --- a/web-player/src/routes/api/tracks/[id]/stream/+server.ts +++ b/web-player/src/routes/api/tracks/[id]/stream/+server.ts @@ -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 } } }); diff --git a/web-player/src/routes/api/users/[id]/[list=followList]/+server.ts b/web-player/src/routes/api/users/[id]/[list=followList]/+server.ts index 6ccd838..6fc519a 100644 --- a/web-player/src/routes/api/users/[id]/[list=followList]/+server.ts +++ b/web-player/src/routes/api/users/[id]/[list=followList]/+server.ts @@ -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, diff --git a/web-player/src/routes/auth/callback/+server.ts b/web-player/src/routes/auth/callback/+server.ts index 97ac6f7..b15abea 100644 --- a/web-player/src/routes/auth/callback/+server.ts +++ b/web-player/src/routes/auth/callback/+server.ts @@ -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.'); }