web-player: normalize tracks, albums and playlists into single domain types at the server boundary
This commit is contained in:
@@ -6,6 +6,7 @@ import {
|
||||
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';
|
||||
import { uploadPresignedImage } from '$lib/server/upload';
|
||||
@@ -18,7 +19,7 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: ALBUMS_PAGE_SIZE } }
|
||||
});
|
||||
|
||||
const albums = unwrapOrError(result, 'No se pudieron cargar tus álbumes.');
|
||||
const albums = toPage(unwrapOrError(result, 'No se pudieron cargar tus álbumes.'), toAlbum);
|
||||
|
||||
return { albums };
|
||||
};
|
||||
|
||||
@@ -40,10 +40,7 @@
|
||||
<MediaCard
|
||||
href="/albums/{album.id}"
|
||||
title={album.title}
|
||||
subtitle={albumMeta(
|
||||
album.releaseYear === null ? undefined : Number(album.releaseYear),
|
||||
Number(album.trackCount)
|
||||
)}
|
||||
subtitle={albumMeta(album.releaseYear, album.trackCount)}
|
||||
trackIds={album.coverTrackIds}
|
||||
src="/api/albums/{album.id}/cover?size=large"
|
||||
index={i}
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
unwrapOrError,
|
||||
unwrapOrFail
|
||||
} from '$lib/server/api';
|
||||
import { toAlbum, toTrack } from '$lib/server/mappers';
|
||||
import { ALBUM_TRACKS_PAGE_SIZE, LIBRARY_PICKER_PAGE_SIZE } from '$lib/config';
|
||||
import { parseAlbumForm } from '$lib/server/albumForm';
|
||||
import { uploadPresignedImage } from '$lib/server/upload';
|
||||
@@ -34,11 +35,13 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
: Promise.resolve(null)
|
||||
]);
|
||||
|
||||
const album = unwrapOrError(albumRes, 'Álbum no encontrado.', 404);
|
||||
const album = toAlbum(unwrapOrError(albumRes, 'Álbum no encontrado.', 404));
|
||||
|
||||
const tracks = tracksRes.data?.items ?? [];
|
||||
const tracks = (tracksRes.data?.items ?? []).map(toTrack);
|
||||
const inAlbum = new Set(tracks.map((track) => track.id));
|
||||
const library = (libraryRes?.data?.items ?? []).filter((track) => !inAlbum.has(track.id));
|
||||
const library = (libraryRes?.data?.items ?? [])
|
||||
.filter((track) => !inAlbum.has(track.id))
|
||||
.map(toTrack);
|
||||
|
||||
const isOwner = user !== null && String(album.ownerUserId) === user.sub;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import Plus from '@lucide/svelte/icons/plus';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
@@ -28,15 +28,15 @@
|
||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
||||
|
||||
function playAll() {
|
||||
playAllOrToggle(toQueueItems(tracks));
|
||||
playAllOrToggle(tracks);
|
||||
}
|
||||
|
||||
function playTrackFrom(index: number) {
|
||||
player.playOrToggle(toQueueItems(tracks), index);
|
||||
player.playOrToggle(tracks, index);
|
||||
}
|
||||
|
||||
function playLibraryFrom(index: number) {
|
||||
player.playOrToggle(toQueueItems(library), index);
|
||||
player.playOrToggle(library, index);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -55,10 +55,7 @@
|
||||
eyebrow="Álbum"
|
||||
title={album.title}
|
||||
description={album.description ?? undefined}
|
||||
meta={albumMeta(
|
||||
album.releaseYear === null ? undefined : Number(album.releaseYear),
|
||||
tracks.length
|
||||
)}
|
||||
meta={albumMeta(album.releaseYear, tracks.length)}
|
||||
>
|
||||
{#snippet cover()}
|
||||
<Artwork
|
||||
@@ -118,7 +115,7 @@
|
||||
action="?/edit"
|
||||
initialTitle={album.title}
|
||||
initialDescription={album.description ?? ''}
|
||||
initialReleaseYear={album.releaseYear === null ? undefined : Number(album.releaseYear)}
|
||||
initialReleaseYear={album.releaseYear ?? undefined}
|
||||
coverFallbackUrl="/api/albums/{album.id}/cover?size=medium"
|
||||
formMessage={form?.message}
|
||||
submitLabel="Guardar"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { createApiClient, 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,
|
||||
@@ -48,14 +49,16 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => {
|
||||
usersPromise
|
||||
]);
|
||||
|
||||
const tracks = unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.');
|
||||
const tracks = toPage(unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.'), (item) =>
|
||||
toTrack(item.track)
|
||||
);
|
||||
|
||||
return {
|
||||
query,
|
||||
genre,
|
||||
genres,
|
||||
tracks,
|
||||
albums: (albumsRes?.data?.items ?? []).map((item) => item.album),
|
||||
albums: (albumsRes?.data?.items ?? []).map((item) => toAlbum(item.album)),
|
||||
albumsTotal: Number(albumsRes?.data?.totalItemCount ?? 0),
|
||||
users: usersRes?.data?.items ?? [],
|
||||
usersTotal: Number(usersRes?.data?.totalItemCount ?? 0),
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<script lang="ts">
|
||||
import Music from '@lucide/svelte/icons/music';
|
||||
import { player, toQueueItem } from '$lib/player/player.svelte';
|
||||
import { fetchAlbumQueueItems } from '$lib/data/albums';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { fetchAlbumTracks } from '$lib/data/albums';
|
||||
import type { Track } from '$lib/types';
|
||||
import { fmtTime, fmtPlays, plural } from '$lib/utils/format';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
@@ -49,9 +50,7 @@
|
||||
let albumMenu = $state<AlbumMenuState | null>(null);
|
||||
let sfilter = $state<SearchFilter>('Todo');
|
||||
|
||||
type LocalTrack = (typeof data.tracks.items)[number]['track'];
|
||||
|
||||
let localItems = $state<LocalTrack[]>([]);
|
||||
let localItems = $state<Track[]>([]);
|
||||
let localPage = $state(1);
|
||||
let hasMore = $state(false);
|
||||
let loadingMore = $state(false);
|
||||
@@ -60,19 +59,12 @@
|
||||
trackMenu.close();
|
||||
albumMenu = null;
|
||||
sfilter = data.genre ? 'Canciones' : 'Todo';
|
||||
localItems = data.tracks.items.map((item) => item.track);
|
||||
localPage = Number(data.tracks.pageNumber);
|
||||
localItems = data.tracks.items;
|
||||
localPage = data.tracks.pageNumber;
|
||||
hasMore = data.tracks.hasNextPage;
|
||||
});
|
||||
|
||||
const songRows = $derived(
|
||||
localItems.map((track) => ({
|
||||
track,
|
||||
seconds: Number(track.duration)
|
||||
}))
|
||||
);
|
||||
|
||||
const items = $derived(songRows.map((row) => row.track));
|
||||
const items = $derived(localItems);
|
||||
|
||||
const tiles = $derived(genreTiles(data.genres));
|
||||
const activeGenre = $derived(data.genre ? genreInfo(data.genre) : null);
|
||||
@@ -92,7 +84,7 @@
|
||||
|
||||
const counts = $derived(
|
||||
searchCounts({
|
||||
tracks: Number(data.tracks.totalItemCount),
|
||||
tracks: data.tracks.totalItemCount,
|
||||
albums: data.albumsTotal,
|
||||
playlists: playlistMatches.length,
|
||||
users: data.usersTotal
|
||||
@@ -106,7 +98,7 @@
|
||||
type TopResult = NonNullable<
|
||||
ReturnType<
|
||||
typeof findTopResult<
|
||||
LocalTrack,
|
||||
Track,
|
||||
(typeof albums)[number],
|
||||
(typeof playlistMatches)[number],
|
||||
(typeof users)[number]
|
||||
@@ -128,7 +120,7 @@
|
||||
|
||||
function playTopResult() {
|
||||
if (topResult?.kind !== 'track') return;
|
||||
player.playOrToggle([toQueueItem(topResult.track)], 0);
|
||||
player.playOrToggle([topResult.track], 0);
|
||||
}
|
||||
|
||||
const topResultHref = $derived.by(() => {
|
||||
@@ -139,11 +131,7 @@
|
||||
});
|
||||
|
||||
function togglePlay(index: number) {
|
||||
player.playOrToggle(items.map(toQueueItem), index);
|
||||
}
|
||||
|
||||
function openContextMenu(event: MouseEvent, track: LocalTrack) {
|
||||
trackMenu.open(event, track);
|
||||
player.playOrToggle(items, index);
|
||||
}
|
||||
|
||||
function openAlbumMenu(event: MouseEvent, albumId: string) {
|
||||
@@ -154,14 +142,14 @@
|
||||
if (!albumMenu) return;
|
||||
const { albumId } = albumMenu;
|
||||
albumMenu = null;
|
||||
player.playNext(await fetchAlbumQueueItems(albumId));
|
||||
player.playNext(await fetchAlbumTracks(albumId));
|
||||
}
|
||||
|
||||
async function albumAddToQueue() {
|
||||
if (!albumMenu) return;
|
||||
const { albumId } = albumMenu;
|
||||
albumMenu = null;
|
||||
player.appendToQueue(await fetchAlbumQueueItems(albumId));
|
||||
player.appendToQueue(await fetchAlbumTracks(albumId));
|
||||
}
|
||||
|
||||
async function loadMore() {
|
||||
@@ -177,12 +165,8 @@
|
||||
const res = await fetch(`/api/tracks?${params}`);
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const next = (await res.json()) as typeof data.tracks;
|
||||
localItems = appendUnique(
|
||||
localItems,
|
||||
next.items.map((item) => item.track),
|
||||
(track) => track.id
|
||||
);
|
||||
localPage = Number(next.pageNumber);
|
||||
localItems = appendUnique(localItems, next.items, (track) => track.id);
|
||||
localPage = next.pageNumber;
|
||||
hasMore = next.hasNextPage;
|
||||
} catch {
|
||||
hasMore = false;
|
||||
@@ -340,30 +324,30 @@
|
||||
<div class="mt-9 flex flex-col gap-9">
|
||||
{#if showGroup(sfilter, 'Canciones')}
|
||||
<div>
|
||||
{#if songRows.length > 0}
|
||||
{#if items.length > 0}
|
||||
<SectionHeading title="Canciones" count={counts.Canciones} />
|
||||
<ul class="flex flex-col gap-1">
|
||||
{#each capped(sfilter, songRows) as { track, seconds }, i (track.id)}
|
||||
{#each capped(sfilter, items) as track, i (track.id)}
|
||||
<li>
|
||||
<ListRow
|
||||
title={track.title}
|
||||
subtitle={track.artist}
|
||||
subtitleHref={track.ownerUserId}
|
||||
explicit={track.isExplicit}
|
||||
active={player.current.id === track.id}
|
||||
explicit={track.explicit}
|
||||
active={player.currentId === track.id}
|
||||
size="lg"
|
||||
trackId={track.id}
|
||||
onclick={() => togglePlay(i)}
|
||||
oncontextmenu={(e) => openContextMenu(e, track)}
|
||||
oncontextmenu={(e) => trackMenu.open(e, track)}
|
||||
>
|
||||
{#snippet overlay()}
|
||||
{#if player.current.id === track.id}
|
||||
{#if player.currentId === track.id}
|
||||
<EqBars overlay paused={!player.playing} />
|
||||
{/if}
|
||||
{/snippet}
|
||||
{#snippet trailing()}
|
||||
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||
{fmtTime(seconds)} · {fmtPlays(track.listensCount)}
|
||||
{fmtTime(track.duration)} · {fmtPlays(track.listensCount)}
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
@@ -400,7 +384,7 @@
|
||||
>
|
||||
{#snippet trailing()}
|
||||
<span class="hidden shrink-0 text-xs text-fg-2 tabular-nums sm:block">
|
||||
{plural(Number(album.trackCount), 'canción', 'canciones')}
|
||||
{plural(album.trackCount, 'canción', 'canciones')}
|
||||
</span>
|
||||
{/snippet}
|
||||
</ListRow>
|
||||
@@ -459,7 +443,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}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { fail } from '@sveltejs/kit';
|
||||
import { toDatedTrack, toPage } from '$lib/server/mappers';
|
||||
import {
|
||||
createApiClient,
|
||||
requireAccessTokenAction,
|
||||
@@ -17,7 +18,9 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: PAGE_SIZE } }
|
||||
});
|
||||
|
||||
return { tracks: unwrapOrError(result, 'No se pudo cargar tu biblioteca.') };
|
||||
return {
|
||||
tracks: toPage(unwrapOrError(result, 'No se pudo cargar tu biblioteca.'), toDatedTrack)
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import Upload from '@lucide/svelte/icons/upload';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import Music from '@lucide/svelte/icons/music';
|
||||
import { player, toQueueItems } from '$lib/player/player.svelte';
|
||||
import { player } from '$lib/player/player.svelte';
|
||||
import { plural } from '$lib/utils/format';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
@@ -14,13 +14,12 @@
|
||||
let { data } = $props();
|
||||
|
||||
const items = $derived(data.tracks.items);
|
||||
const total = $derived(Number(data.tracks.totalItemCount));
|
||||
const listTracks = $derived(items.map((track) => ({ ...track, uploadedAt: track.createdAt })));
|
||||
const total = $derived(data.tracks.totalItemCount);
|
||||
|
||||
let deleting = $state<(typeof items)[number] | null>(null);
|
||||
|
||||
function togglePlay(index: number) {
|
||||
player.playOrToggle(toQueueItems(items), index);
|
||||
player.playOrToggle(items, index);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -46,7 +45,7 @@
|
||||
|
||||
{#if items.length > 0}
|
||||
<TrackList
|
||||
tracks={listTracks}
|
||||
tracks={items}
|
||||
columns={['uploaded', 'plays']}
|
||||
onPlay={togglePlay}
|
||||
rowAction={{
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
import TrackList from '$lib/components/ui/media/TrackList.svelte';
|
||||
|
||||
const tracks = $derived(liked.list);
|
||||
const listTracks = $derived(tracks.map((track) => ({ ...track, addedAt: track.likedAt })));
|
||||
const listTracks = $derived(tracks.map((track) => ({ ...track, date: track.likedAt })));
|
||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
||||
|
||||
function playFrom(index: number) {
|
||||
@@ -55,7 +55,7 @@
|
||||
columns={['added', 'plays']}
|
||||
onPlay={playFrom}
|
||||
rowAction={{
|
||||
onclick: (track) => liked.toggle({ ...track, artist: track.artist ?? undefined }),
|
||||
onclick: (track) => liked.toggle(track),
|
||||
icon: X,
|
||||
label: 'Quitar de Me gusta'
|
||||
}}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
requireUser,
|
||||
unwrapOrError
|
||||
} from '$lib/server/api';
|
||||
import { toPlaylistSummary, toTrack } from '$lib/server/mappers';
|
||||
import { uploadPresignedImage } from '$lib/server/upload';
|
||||
|
||||
const MAX_NAME = 100;
|
||||
@@ -25,24 +26,21 @@ export const load: PageServerLoad = async ({ locals, url, fetch }) => {
|
||||
.GET('/playlists/{playlistId}/tracks', {
|
||||
params: { path: { playlistId: playlist.id }, query: { pageNumber: 1, pageSize: 500 } }
|
||||
})
|
||||
.then((res) => res.data?.items ?? [])
|
||||
.then((res) => (res.data?.items ?? []).map(toTrack))
|
||||
)
|
||||
);
|
||||
|
||||
const items = data.items.map((playlist, i) => {
|
||||
const tracks = tracksByPlaylist[i];
|
||||
const trackCount = tracks.length;
|
||||
const durationSeconds = tracks.reduce((sum, t) => sum + Number(t.duration || 0), 0);
|
||||
return { ...playlist, trackCount, durationSeconds };
|
||||
});
|
||||
const items = data.items.map((playlist, i) =>
|
||||
toPlaylistSummary(playlist, tracksByPlaylist[i].length)
|
||||
);
|
||||
|
||||
const totals = {
|
||||
playlistCount: items.length,
|
||||
trackCount: items.reduce((sum, p) => sum + p.trackCount, 0),
|
||||
durationSeconds: items.reduce((sum, p) => sum + p.durationSeconds, 0)
|
||||
trackCount: items.reduce((sum, playlist) => sum + playlist.trackCount, 0),
|
||||
durationSeconds: tracksByPlaylist.flat().reduce((sum, track) => sum + track.duration, 0)
|
||||
};
|
||||
|
||||
return { playlists: { ...data, items }, totals };
|
||||
return { playlists: items, totals };
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const items = $derived(data.playlists.items);
|
||||
const items = $derived(data.playlists);
|
||||
const totals = $derived(data.totals);
|
||||
const summary = $derived(
|
||||
`${plural(totals.playlistCount, 'playlist', 'playlists')} · ${plural(totals.trackCount, 'canción', 'canciones')} · ${fmtDurationLong(totals.durationSeconds)}`
|
||||
@@ -40,7 +40,7 @@
|
||||
<MediaCard
|
||||
href="/playlists/{playlist.id}"
|
||||
title={playlist.name}
|
||||
subtitle={playlistMeta(Number(playlist.trackCount))}
|
||||
subtitle={playlistMeta(playlist.trackCount)}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
unwrapOrError,
|
||||
unwrapOrFail
|
||||
} from '$lib/server/api';
|
||||
import { toDatedTrack, toPlaylist } from '$lib/server/mappers';
|
||||
import { uploadPresignedImage } from '$lib/server/upload';
|
||||
|
||||
const MAX_NAME = 100;
|
||||
@@ -23,8 +24,8 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
})
|
||||
]);
|
||||
|
||||
const playlist = unwrapOrError(playlistRes, 'Playlist no encontrada.', 404);
|
||||
const tracks = tracksRes.data?.items ?? [];
|
||||
const playlist = toPlaylist(unwrapOrError(playlistRes, 'Playlist no encontrada.', 404));
|
||||
const tracks = (tracksRes.data?.items ?? []).map(toDatedTrack);
|
||||
|
||||
const isOwner = user !== null && String(playlist.ownerUserId) === user.sub;
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import ListMusic from '@lucide/svelte/icons/list-music';
|
||||
import X from '@lucide/svelte/icons/x';
|
||||
import { player, toQueueItems, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import { player, isQueueCurrent, playAllOrToggle } from '$lib/player/player.svelte';
|
||||
import Page from '$lib/components/ui/layout/Page.svelte';
|
||||
import PageHeader from '$lib/components/ui/layout/PageHeader.svelte';
|
||||
import Modal from '$lib/components/ui/overlay/Modal.svelte';
|
||||
@@ -19,7 +19,6 @@
|
||||
const playlist = $derived(data.playlist);
|
||||
const tracks = $derived(data.tracks);
|
||||
const isOwner = $derived(data.isOwner);
|
||||
const listTracks = $derived(tracks.map((track) => ({ ...track, addedAt: track.createdAt })));
|
||||
|
||||
let editing = $state(false);
|
||||
let confirmingDelete = $state(false);
|
||||
@@ -27,11 +26,11 @@
|
||||
const isCurrentQueue = $derived(isQueueCurrent(tracks));
|
||||
|
||||
function playAll() {
|
||||
playAllOrToggle(toQueueItems(tracks));
|
||||
playAllOrToggle(tracks);
|
||||
}
|
||||
|
||||
function playFrom(index: number) {
|
||||
player.playOrToggle(toQueueItems(tracks), index);
|
||||
player.playOrToggle(tracks, index);
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -85,7 +84,7 @@
|
||||
|
||||
{#if tracks.length > 0}
|
||||
<TrackList
|
||||
tracks={listTracks}
|
||||
{tracks}
|
||||
columns={['added', 'plays']}
|
||||
onPlay={playFrom}
|
||||
rowAction={isOwner
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PageServerLoad, Actions } from './$types';
|
||||
import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api';
|
||||
import { toPlaylistSummary } from '$lib/server/mappers';
|
||||
import { followUserAction, unfollowUserAction } from '$lib/server/followActions';
|
||||
|
||||
const PROFILE_PLAYLISTS_PAGE_SIZE = 50;
|
||||
@@ -32,14 +33,19 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
)
|
||||
);
|
||||
|
||||
const playlists = playlistItems.map((playlist, i) => ({
|
||||
...playlist,
|
||||
trackCount: tracksByPlaylist[i]
|
||||
}));
|
||||
const playlists = playlistItems.map((playlist, i) =>
|
||||
toPlaylistSummary(playlist, tracksByPlaylist[i])
|
||||
);
|
||||
|
||||
const isOwnProfile = viewer !== null && viewer.sub === params.id;
|
||||
|
||||
return { profile, playlists, isOwnProfile, isAnonymous: viewer === null };
|
||||
return {
|
||||
profile,
|
||||
followersCount: Number(profile.followersCount),
|
||||
playlists,
|
||||
isOwnProfile,
|
||||
isAnonymous: viewer === null
|
||||
};
|
||||
};
|
||||
|
||||
export const actions: Actions = {
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
{/snippet}
|
||||
|
||||
{#snippet followStats()}
|
||||
{@const followers = plural(Number(profile.followersCount), 'seguidor', 'seguidores')}
|
||||
{@const followers = plural(data.followersCount, 'seguidor', 'seguidores')}
|
||||
{#if data.isOwnProfile || profile.canViewFollowers}
|
||||
<a href="/user/{profile.id}/followers" class="transition-colors hover:text-fg">{followers}</a>
|
||||
{:else}
|
||||
@@ -60,7 +60,7 @@
|
||||
<MediaCard
|
||||
href="/playlists/{playlist.id}"
|
||||
title={playlist.name}
|
||||
subtitle={playlistMeta(Number(playlist.trackCount))}
|
||||
subtitle={playlistMeta(playlist.trackCount)}
|
||||
trackIds={playlist.coverTrackIds}
|
||||
src="/api/playlists/{playlist.id}/cover?size=large&v={encodeURIComponent(
|
||||
playlist.updatedAt
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { PageServerLoad } from './$types';
|
||||
import { createApiClient, 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';
|
||||
|
||||
@@ -19,7 +20,9 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
|
||||
return {
|
||||
list: params.list,
|
||||
profile,
|
||||
users: forbidden ? null : unwrapOrError(listRes, 'No se pudo cargar la lista.'),
|
||||
users: forbidden
|
||||
? null
|
||||
: toPage(unwrapOrError(listRes, 'No se pudo cargar la lista.'), (item) => item),
|
||||
forbidden,
|
||||
viewerId: viewer?.sub ?? null
|
||||
};
|
||||
|
||||
@@ -20,13 +20,13 @@
|
||||
|
||||
$effect(() => {
|
||||
items = data.users?.items ?? [];
|
||||
page = Number(data.users?.pageNumber ?? 1);
|
||||
page = data.users?.pageNumber ?? 1;
|
||||
hasMore = data.users?.hasNextPage ?? false;
|
||||
});
|
||||
|
||||
const isFollowers = $derived(data.list === 'followers');
|
||||
const title = $derived(isFollowers ? 'Seguidores' : 'Siguiendo');
|
||||
const total = $derived(Number(data.users?.totalItemCount ?? 0));
|
||||
const total = $derived(data.users?.totalItemCount ?? 0);
|
||||
|
||||
async function loadMore() {
|
||||
if (loadingMore) return;
|
||||
@@ -36,7 +36,7 @@
|
||||
if (!res.ok) throw new Error(String(res.status));
|
||||
const next = (await res.json()) as NonNullable<typeof data.users>;
|
||||
items = appendUnique(items, next.items, (u) => u.id);
|
||||
page = Number(next.pageNumber);
|
||||
page = next.pageNumber;
|
||||
hasMore = next.hasNextPage;
|
||||
} catch {
|
||||
hasMore = false;
|
||||
|
||||
Reference in New Issue
Block a user