feat: bring over the albums, explore and player work from the worktree

Moves the remaining uncommitted work: the home gets quick mixes and a
"Tus álbumes" shelf, saving a YouTube album now goes through
addYoutubeAlbumToPlaylistAction instead of the SaveYouTubeAlbum command,
and UserAlbum drops its unused ExternalId.

The mixes section keeps its current shape (no refresh action, no badge,
hidden when empty) rather than the older version the worktree carried.
This commit is contained in:
2026-07-21 18:18:23 +02:00
parent b75c97a696
commit 2f98982d83
29 changed files with 1742 additions and 406 deletions
@@ -1,5 +1,6 @@
import { fail, type RequestEvent } from '@sveltejs/kit';
import { createApiClient, requireAccessTokenAction } from '$lib/server/api';
import { ALBUM_TRACKS_PAGE_SIZE } from '$lib/config';
export async function addTrackAction({ request, params, locals, fetch }: RequestEvent) {
const accessToken = requireAccessTokenAction(locals);
@@ -59,3 +60,79 @@ export async function addYouTubeToPlaylistAction({ request, params, locals, fetc
return { added: true, videoId, playlistId };
}
export async function addAlbumToPlaylistAction({ request, params, locals, fetch }: RequestEvent) {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
const playlistId = String(form.get('playlistId') ?? '') || String(params.id ?? '');
const albumId = String(form.get('albumId') ?? '');
if (!playlistId || !albumId) {
return fail(400, { message: 'Faltan datos del álbum.' });
}
const api = createApiClient({ fetch, accessToken });
const { data: tracks, error: tracksErr } = await api.GET('/albums/{albumId}/tracks', {
params: { path: { albumId }, query: { pageNumber: 1, pageSize: ALBUM_TRACKS_PAGE_SIZE } }
});
if (tracksErr || !tracks) {
return fail(502, { message: 'No se pudo cargar el álbum.', albumId });
}
let added = 0;
for (const track of tracks.items) {
const { error: err } = await api.POST('/playlists/{playlistId}/tracks/{trackId}', {
params: { path: { playlistId, trackId: String(track.id) } }
});
if (!err) added++;
}
return { addedAlbum: true, albumId, playlistId, added };
}
export async function addYoutubeAlbumToPlaylistAction({
request,
params,
locals,
fetch
}: RequestEvent) {
const accessToken = requireAccessTokenAction(locals);
if (typeof accessToken !== 'string') return accessToken;
const form = await request.formData();
const playlistId = String(form.get('playlistId') ?? '') || String(params.id ?? '');
const albumId = String(form.get('albumId') ?? '');
if (!playlistId || !albumId) {
return fail(400, { message: 'Faltan datos del álbum.' });
}
const api = createApiClient({ fetch, accessToken });
const { data: detail, error: detailErr } = await api.GET('/youtube/albums/{albumId}', {
params: { path: { albumId } }
});
if (detailErr || !detail) {
return fail(502, { message: 'No se pudo cargar el álbum de YouTube.', albumId });
}
let added = 0;
for (const track of detail.tracks) {
const { error: err } = await api.POST('/playlists/{playlistId}/youtube-tracks', {
params: { path: { playlistId } },
body: {
videoId: track.videoId,
title: track.title,
artist: detail.album.artist,
durationSeconds: Number(track.durationSeconds),
thumbnailUrl: detail.album.thumbnailUrl
}
});
if (!err) added++;
}
return { addedAlbum: true, albumId, playlistId, added };
}