web-player: centralize server helpers (apiFor, authedAction, form readers) and unify page sizes
This commit is contained in:
@@ -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