web-player: centralize server helpers (apiFor, authedAction, form readers) and unify page sizes
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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 function requireUser(locals: App.Locals, url: URL): SessionUser {
|
||||
if (!locals.user)
|
||||
export type ApiClient = ReturnType<typeof createApiClient>;
|
||||
|
||||
export function apiFor({ fetch, locals }: Pick<RequestEvent, 'fetch' | 'locals'>): 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) 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<string, string>;
|
||||
locals: App.Locals;
|
||||
url: URL;
|
||||
}
|
||||
|
||||
export function authedAction<R>(
|
||||
handler: (context: ActionContext) => R | Promise<R>,
|
||||
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<string, string>,
|
||||
locals: event.locals,
|
||||
url: event.url
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function unwrapOrError<T>(
|
||||
@@ -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<T>(
|
||||
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 };
|
||||
}
|
||||
|
||||
@@ -36,9 +36,9 @@ export function getOidcConfig(): Promise<client.Configuration> {
|
||||
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<Session | null>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
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 form = await request.formData();
|
||||
const id = String(form.get('userId') ?? '') || String(params.id ?? '');
|
||||
const setFollowing = (follow: boolean) =>
|
||||
authedAction(async ({ api, params }) => {
|
||||
const id = params.id ?? '';
|
||||
if (!id) return fail(400, { message: 'Falta el usuario.' });
|
||||
|
||||
const api = createApiClient({ fetch, accessToken });
|
||||
const options = { params: { path: { id } } };
|
||||
const { error } = follow
|
||||
? await api.POST('/users/{id}/follow', options)
|
||||
@@ -22,7 +18,7 @@ async function setFollowing({ request, params, locals, fetch }: RequestEvent, fo
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<typeof createApiClient>;
|
||||
|
||||
export function fetchFollowList(
|
||||
api: ApiClient,
|
||||
list: FollowList,
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 } } }),
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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 },
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user