web-player: normalize tracks, albums and playlists into single domain types at the server boundary

This commit is contained in:
2026-09-24 05:09:37 +02:00
parent 37fc229bf6
commit 5b2a0e4d46
41 changed files with 526 additions and 452 deletions
@@ -1,5 +1,6 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, 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 }) => {
@@ -8,7 +9,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
const mixRes = await api.GET('/mixes/{id}', { params: { path: { id: params.id } } });
return { mix: unwrapOrError(mixRes, 'Mezcla no encontrada.', 404) };
return { mix: toMix(unwrapOrError(mixRes, 'Mezcla no encontrada.', 404)) };
};
export const actions: Actions = {
@@ -1,12 +1,6 @@
<script lang="ts">
import Shuffle from '@lucide/svelte/icons/shuffle';
import {
player,
isQueueCurrent,
playAllOrToggle,
playShuffled,
toQueueItem
} from '$lib/player/player.svelte';
import { player, isQueueCurrent, playAllOrToggle, playShuffled } from '$lib/player/player.svelte';
import { fmtTime, plural } from '$lib/utils/format';
import Page from '$lib/components/ui/layout/Page.svelte';
import BackLink from '$lib/components/ui/primitives/BackLink.svelte';
@@ -18,33 +12,19 @@
import ContextMenu from '$lib/components/ui/overlay/ContextMenu.svelte';
import ListPlus from '@lucide/svelte/icons/list-plus';
import { createTrackMenu } from '$lib/state/menus.svelte';
import { mixItemTrack } from '$lib/data/mixes';
let { data, form } = $props();
const mix = $derived(data.mix);
const items = $derived(mix.items);
const tracks = $derived(items.map(mixItemTrack));
const queue = $derived(tracks.map(toQueueItem));
const listTracks = $derived(
items.map((item) => ({
id: item.trackId,
title: item.title,
artist: item.artist,
duration: item.durationSeconds,
listensCount: item.listensCount
}))
);
const tracks = $derived(mix.tracks);
const totalSeconds = $derived(
items.reduce((total, item) => total + Number(item.durationSeconds), 0)
);
const isCurrentQueue = $derived(isQueueCurrent(queue));
const totalSeconds = $derived(tracks.reduce((total, track) => total + track.duration, 0));
const isCurrentQueue = $derived(isQueueCurrent(tracks));
const trackMenu = createTrackMenu();
function playFrom(index: number) {
player.playOrToggle(queue, index);
player.playOrToggle(tracks, index);
}
</script>
@@ -60,18 +40,18 @@
eyebrow="Mezcla"
title={mix.title}
description={mix.subtitle ?? undefined}
meta="{plural(items.length, 'canción', 'canciones')} · {fmtTime(totalSeconds)}"
meta="{plural(tracks.length, 'canción', 'canciones')} · {fmtTime(totalSeconds)}"
>
{#snippet cover()}
<Artwork
trackIds={items.map((item) => item.trackId)}
trackIds={tracks.map((track) => track.id)}
size="hero"
alt={mix.title}
class="shrink-0"
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={() => playAllOrToggle(queue)} disabled={queue.length === 0}>
<Button size="sm" onclick={() => playAllOrToggle(tracks)} disabled={tracks.length === 0}>
{#if isCurrentQueue && player.playing}
Pausar
{:else}
@@ -81,8 +61,8 @@
<Button
variant="secondary"
size="sm"
onclick={() => playShuffled(queue)}
disabled={queue.length === 0}
onclick={() => playShuffled(tracks)}
disabled={tracks.length === 0}
>
<Shuffle class="size-4" />
Aleatorio
@@ -95,7 +75,7 @@
{/if}
<TrackList
tracks={listTracks}
{tracks}
columns={['plays']}
onPlay={playFrom}
oncontextmenu={(e, _track, i) => trackMenu.open(e, tracks[i])}
@@ -113,7 +93,7 @@
]}
playlistAction={{
action: '?/addTrack',
fields: { trackId: String(trackMenu.state.track.id) },
fields: { trackId: trackMenu.state.track.id },
label: 'Añadir a una playlist'
}}
playlists={data.userPlaylists}