feat(web-player): page the album results and save YouTube albums

The albums section keeps a Ver más button while YouTube Music has another
page, fed by a new /api/youtube/albums route that forwards the continuation
token. The YouTube album page gains a save button that drops the whole album,
tracks included, into your library and takes you to it.
This commit is contained in:
2026-07-20 20:49:56 +02:00
parent df1cd5f1e9
commit 62b0ab37a9
6 changed files with 182 additions and 4 deletions
@@ -1,5 +1,11 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api';
import { fail, redirect } from '@sveltejs/kit';
import {
createApiClient,
requireAccessTokenAction,
requireUser,
unwrapOrError
} from '$lib/server/api';
import { addYouTubeToPlaylistAction } from '$lib/server/playlistActions';
export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
@@ -19,5 +25,28 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => {
};
export const actions: Actions = {
addYouTubeToPlaylist: addYouTubeToPlaylistAction
addYouTubeToPlaylist: addYouTubeToPlaylistAction,
save: async ({ params, locals, fetch }) => {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const api = createApiClient({ fetch, accessToken });
const {
data,
error: err,
response
} = await api.POST('/albums/youtube/{albumId}', {
params: { path: { albumId: params.albumId } }
});
if (response?.status === 409) {
return fail(409, { message: 'Ese álbum ya está en tu biblioteca.' });
}
if (err || !data) {
return fail(502, { message: 'No se pudo guardar el álbum.' });
}
redirect(303, `/albums/${data.id}`);
}
};
@@ -1,13 +1,16 @@
<script lang="ts">
import { enhance } from '$app/forms';
import ArrowLeft from '@lucide/svelte/icons/arrow-left';
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import Plus from '@lucide/svelte/icons/plus';
import { player, type QueueItem } from '$lib/player/player.svelte';
import { hueFor } from '$lib/theme/color';
import { fmtTime } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import Button from '$lib/components/ui/Button.svelte';
import Cover from '$lib/components/ui/Cover.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
import TrackRow from '$lib/components/ui/TrackRow.svelte';
import TrackMeta from '$lib/components/ui/TrackMeta.svelte';
@@ -19,12 +22,13 @@
} from '$lib/components/TrackContextMenu.svelte';
import type { TrackTarget } from '$lib/tracks';
let { data } = $props();
let { data, form } = $props();
const album = $derived(data.album.album);
const tracks = $derived(data.album.tracks);
let contextMenu = $state<ContextMenuState | null>(null);
let saving = $state(false);
const queue = $derived<QueueItem[]>(
tracks.map((track) => ({
@@ -123,8 +127,28 @@
Reproducir
{/if}
</Button>
<form
method="POST"
action="?/save"
use:enhance={() => {
saving = true;
return async ({ update }) => {
await update();
saving = false;
};
}}
>
<Button type="submit" size="sm" variant="secondary" loading={saving}>
<Plus class="h-4 w-4" strokeWidth={2} />
Guardar en mi biblioteca
</Button>
</form>
</div>
{#if form?.message}
<Alert tone="danger" class="mt-4">{form.message}</Alert>
{/if}
{#if data.album.description}
<p class="mt-6 max-w-3xl text-sm text-fg-3">{data.album.description}</p>
{/if}