Remove YouTube integration from web-player; unify Home track rows

Drops every YouTube-backed route, proxy and type (external
albums/tracks, thumbnail proxy, youtube.ts) and simplifies
player/track/mix types to the local-only catalog now that the
backend contract dropped Source/ExternalId. Genre tiles on Explore
and "Populares esta semana" on Home stay as deliberate client-only
placeholders per product decision, just without the YouTube backing.

Also finishes the design-system unification pass: Home's three
hand-rolled track rows (Continuar escuchando, Playlist destacada,
Populares esta semana) now reuse the shared TrackTitleCell component
instead of duplicating cover+title+artist markup, and the orphaned
Rail.svelte (no importers) is removed.
This commit is contained in:
2026-09-15 21:17:14 +02:00
parent 31032bd8e8
commit 5ae9955bd0
41 changed files with 167 additions and 1541 deletions
@@ -38,9 +38,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch, parent
const tracks = tracksRes.data?.items ?? [];
const inAlbum = new Set(tracks.map((track) => track.id));
const library = (libraryRes?.data?.items ?? []).filter(
(track) => track.source === 'Local' && !inAlbum.has(track.id)
);
const library = (libraryRes?.data?.items ?? []).filter((track) => !inAlbum.has(track.id));
const isOwner = user !== null && String(album.ownerUserId) === user.sub;
@@ -1,5 +1,5 @@
<script lang="ts">
import { player, queueIdForTrack, toQueueItems } from '$lib/player/player.svelte';
import { player, toQueueItems } from '$lib/player/player.svelte';
import Page from '$lib/components/ui/Page.svelte';
import Modal from '$lib/components/ui/Modal.svelte';
import Alert from '$lib/components/ui/Alert.svelte';
@@ -19,9 +19,7 @@
let editing = $state(false);
let confirmingDelete = $state(false);
const isCurrentQueue = $derived(
tracks.some((track) => queueIdForTrack(track) === player.current.id)
);
const isCurrentQueue = $derived(tracks.some((track) => track.id === player.current.id));
function playAll() {
if (tracks.length === 0) return;
@@ -1,12 +1,7 @@
<script lang="ts">
import { enhance } from '$app/forms';
import X from '@lucide/svelte/icons/x';
import {
player,
queueIdForTrack,
toQueueItems,
type ApiTrackLike
} from '$lib/player/player.svelte';
import { player, toQueueItems, type ApiTrackLike } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
import IconButton from '$lib/components/ui/IconButton.svelte';
import TrackTable from '$lib/components/ui/TrackTable.svelte';
@@ -31,7 +26,7 @@
<TrackTable index action={isOwner} class="mt-6 sm:mt-8">
{#each tracks as track, i (track.id)}
{@const active = player.current.id === queueIdForTrack(track)}
{@const active = player.current.id === track.id}
<TrackRow {active}>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<TrackTitleCell
@@ -1,32 +0,0 @@
import type { PageServerLoad, Actions } from './$types';
import { createApiClient, optionalUser, unwrapOrError } from '$lib/server/api';
import {
addYouTubeToPlaylistAction,
addYoutubeAlbumToPlaylistAction
} from '$lib/server/playlistActions';
export const load: PageServerLoad = async ({ params, locals, url, fetch, parent }) => {
const { allowAnonymousListening } = await parent();
const user = optionalUser(locals, url, allowAnonymousListening);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
const [albumRes, playlistsRes] = await Promise.all([
api.GET('/albums/external/{source}/{externalId}', {
params: { path: { source: params.source, externalId: params.externalId } }
}),
user
? api.GET('/playlists/users/{userId}', {
params: { path: { userId: user.sub }, query: { pageNumber: 1, pageSize: 50 } }
})
: Promise.resolve(null)
]);
const album = unwrapOrError(albumRes, 'No se pudo cargar el álbum.', 404);
return { album, playlists: playlistsRes?.data?.items ?? [], section: null };
};
export const actions: Actions = {
addYouTubeToPlaylist: addYouTubeToPlaylistAction,
addYoutubeAlbumToPlaylist: addYoutubeAlbumToPlaylistAction
};
@@ -1,179 +0,0 @@
<script lang="ts">
import Play from '@lucide/svelte/icons/play';
import Pause from '@lucide/svelte/icons/pause';
import { player, type QueueItem } from '$lib/player/player.svelte';
import { fmtTime } from '$lib/format';
import Page from '$lib/components/ui/Page.svelte';
import BackLink from '$lib/components/ui/BackLink.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 CollectionHeader from '$lib/components/ui/CollectionHeader.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';
import TrackIndexCell from '$lib/components/ui/TrackIndexCell.svelte';
import TrackTitleCell from '$lib/components/ui/TrackTitleCell.svelte';
import TrackContextMenu, {
contextMenuStateFor,
type ContextMenuState
} from '$lib/components/TrackContextMenu.svelte';
import AlbumContextMenu, {
albumContextMenuStateFor,
type AlbumMenuState
} from '$lib/components/AlbumContextMenu.svelte';
import type { TrackTarget } from '$lib/tracks';
let { data, form } = $props();
const album = $derived(data.album.album);
const tracks = $derived(data.album.tracks);
let contextMenu = $state<ContextMenuState | null>(null);
let albumMenu = $state<AlbumMenuState | null>(null);
const queue = $derived<QueueItem[]>(
tracks.map((track) => ({
id: track.videoId,
title: track.title,
artist: album.artist,
source: 'youtube' as const,
coverUrl: album.thumbnailUrl,
explicit: track.isExplicit
}))
);
const isCurrentQueue = $derived(tracks.some((track) => track.videoId === player.current.id));
function targetFor(index: number): TrackTarget {
const track = tracks[index];
return {
kind: 'youtube',
song: {
videoId: track.videoId,
title: track.title,
artist: album.artist,
album: album.title,
durationSeconds: Number(track.durationSeconds),
thumbnailUrl: album.thumbnailUrl,
isExplicit: track.isExplicit
}
};
}
function playFrom(index: number) {
player.playOrToggle(queue, index);
}
function playAll() {
if (queue.length === 0) return;
if (isCurrentQueue) player.toggle();
else player.playQueue(queue, 0);
}
</script>
<svelte:head>
<title>{album.title}</title>
<meta name="description" content="Álbum {album.title} de {album.artist}," />
</svelte:head>
<Page>
<BackLink href="/explore" label="Volver a explorar" />
<CollectionHeader
eyebrow={album.isSingle ? 'Single' : album.isEp ? 'EP' : 'Álbum'}
title={album.title}
description={album.artist}
meta={[
album.releaseYear ? String(album.releaseYear) : undefined,
`${tracks.length} ${tracks.length === 1 ? 'canción' : 'canciones'}`,
fmtTime(Number(data.album.totalDurationSeconds))
]
.filter((part) => !!part)
.join(' · ')}
>
{#snippet cover()}
<Cover
trackId={album.albumId}
src={album.thumbnailUrl}
size="large"
alt={album.title}
class="h-36 w-36 shrink-0 rounded-[18px] shadow-cover-lg sm:h-41 sm:w-41"
oncontextmenu={(e) => (albumMenu = albumContextMenuStateFor(e, 'youtube', album.albumId))}
/>
{/snippet}
{#snippet actions()}
<Button size="sm" onclick={playAll} disabled={queue.length === 0}>
{#if isCurrentQueue && player.playing}
<Pause class="h-4 w-4" strokeWidth={1.5} />
Pausar
{:else}
<Play class="h-4 w-4" strokeWidth={1.5} />
Reproducir
{/if}
</Button>
{/snippet}
</CollectionHeader>
{#if form?.message}
<Alert tone="danger" class="mt-4">{form.message}</Alert>
{/if}
{#if data.album.description}
<p class="mt-6 text-sm text-fg-3">{data.album.description}</p>
{/if}
<TrackTable index class="mt-6 sm:mt-8">
{#each tracks as track, i (track.videoId)}
{@const active = player.current.id === track.videoId}
<TrackRow
{active}
oncontextmenu={(e) => (contextMenu = contextMenuStateFor(e, targetFor(i)))}
>
<TrackIndexCell index={i} {active} playing={player.playing} onToggle={() => playFrom(i)} />
<TrackTitleCell
trackId={track.videoId}
title={track.title}
artist={album.artist}
coverSrc={album.thumbnailUrl}
explicit={track.isExplicit}
{active}
onClick={() => playFrom(i)}
/>
<TrackMeta>{fmtTime(Number(track.durationSeconds))}</TrackMeta>
</TrackRow>
{/each}
</TrackTable>
</Page>
{#if contextMenu}
<TrackContextMenu
menu={contextMenu}
playlists={data.playlists}
onClose={() => (contextMenu = null)}
onAddToQueue={() => {
const menu = contextMenu;
if (menu?.kind === 'youtube') {
const index = tracks.findIndex((track) => track.videoId === menu.song.videoId);
if (index >= 0) player.addToQueue(queue[index]);
}
contextMenu = null;
}}
/>
{/if}
{#if albumMenu}
<AlbumContextMenu
menu={albumMenu}
playlists={data.playlists}
onClose={() => (albumMenu = null)}
onPlayNext={() => {
player.playNext(queue);
albumMenu = null;
}}
onAddToQueue={() => {
player.appendToQueue(queue);
albumMenu = null;
}}
/>
{/if}