feat(web-player): add the albums pages
A grid of your albums with a create dialog, and a detail page with the tracklist, inline editing and a picker that lists the songs you uploaded so they can be dropped into the album. Covers reuse the playlist mosaic since an album has no artwork of its own yet.
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
import { ALBUM_EARLIEST_YEAR } from '$lib/config';
|
||||
|
||||
interface AlbumFormBody {
|
||||
title: string;
|
||||
description: string | null;
|
||||
releaseYear: number | null;
|
||||
}
|
||||
|
||||
export function parseAlbumForm(form: FormData): { body: AlbumFormBody } | { failMessage: string } {
|
||||
const title = String(form.get('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();
|
||||
if (description.length > 256) {
|
||||
return { failMessage: 'La descripción no puede pasar de 256 caracteres.' };
|
||||
}
|
||||
|
||||
const rawYear = String(form.get('releaseYear') ?? '').trim();
|
||||
const latestYear = new Date().getFullYear() + 1;
|
||||
let releaseYear: number | null = null;
|
||||
|
||||
if (rawYear !== '') {
|
||||
const parsed = Number(rawYear);
|
||||
if (!Number.isInteger(parsed) || parsed < ALBUM_EARLIEST_YEAR || parsed > latestYear) {
|
||||
return { failMessage: `El año debe estar entre ${ALBUM_EARLIEST_YEAR} y ${latestYear}.` };
|
||||
}
|
||||
releaseYear = parsed;
|
||||
}
|
||||
|
||||
return {
|
||||
body: {
|
||||
title,
|
||||
description: description === '' ? null : description,
|
||||
releaseYear
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -53,6 +53,21 @@ export function unwrapOrFail(
|
||||
return result.error ? fail(502, { message }) : undefined;
|
||||
}
|
||||
|
||||
export async function albumCoverTrackIds(
|
||||
api: ReturnType<typeof createApiClient>,
|
||||
albumIds: string[]
|
||||
): Promise<Record<string, string[]>> {
|
||||
const covers = await Promise.all(
|
||||
albumIds.map(async (albumId) => {
|
||||
const { data: tracks } = await api.GET('/albums/{albumId}/tracks', {
|
||||
params: { path: { albumId }, query: { pageNumber: 1, pageSize: 4 } }
|
||||
});
|
||||
return [albumId, tracks?.items.map((t) => t.id) ?? []] as const;
|
||||
})
|
||||
);
|
||||
return Object.fromEntries(covers);
|
||||
}
|
||||
|
||||
export async function playlistCoverTrackIds(
|
||||
api: ReturnType<typeof createApiClient>,
|
||||
playlistIds: string[]
|
||||
|
||||
Reference in New Issue
Block a user