feat(frontend): unified explore search across local and YouTube Music

Explore now has Todo/Mi musica/YouTube Music filters (searching both by default), YouTube results share the local grid card style, and cards show the artist instead of the upload date.
This commit is contained in:
2026-07-16 06:02:57 +02:00
parent 74dd1e8374
commit 8d19272f35
2 changed files with 240 additions and 227 deletions
@@ -4,48 +4,59 @@ import { createApiClient } from '$lib/server/api';
const PAGE_SIZE = 24;
type Source = 'all' | 'local' | 'yt';
function parseSource(value: string | null): Source {
return value === 'local' || value === 'yt' ? value : 'all';
}
export const load: PageServerLoad = async ({ url, locals, fetch }) => {
const query = url.searchParams.get('q')?.trim() ?? '';
const src = url.searchParams.get('src') === 'yt' ? 'yt' : ('local' as const);
const src = parseSource(url.searchParams.get('src'));
const page = Math.max(1, Number(url.searchParams.get('page')) || 1);
const api = createApiClient({ fetch, accessToken: locals.accessToken ?? undefined });
if (src === 'yt') {
const emptyResults = { items: [], continuationToken: '' };
const wantsLocal = src !== 'yt';
const wantsYouTube = src !== 'local';
const playlistsPromise = locals.user
const tracksPromise = wantsLocal
? api.GET('/tracks', {
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
})
: Promise.resolve(null);
const searchPromise =
wantsYouTube && query && locals.accessToken
? api.GET('/youtube/search', { params: { query: { query } } })
: Promise.resolve(null);
const playlistsPromise =
wantsYouTube && locals.user
? api.GET('/playlists/users/{userId}', {
params: { path: { userId: locals.user.sub }, query: { pageNumber: 1, pageSize: 50 } }
})
: Promise.resolve(null);
const searchPromise =
query && locals.accessToken
? api.GET('/youtube/search', { params: { query: { query } } })
: Promise.resolve(null);
const [tracksRes, searchRes, playlistsRes] = await Promise.all([
tracksPromise,
searchPromise,
playlistsPromise
]);
const [playlistsRes, searchRes] = await Promise.all([playlistsPromise, searchPromise]);
return {
src: 'yt' as const,
query,
ytResults: searchRes?.data ?? emptyResults,
ytError: Boolean(searchRes?.error),
playlists: playlistsRes?.data?.items ?? [],
needsAuth: !locals.user
};
}
const { data, error: err } = await api.GET('/tracks', {
params: { query: { name: query || undefined, pageNumber: page, pageSize: PAGE_SIZE } }
});
if (err || !data) {
if (wantsLocal && (tracksRes?.error || !tracksRes?.data)) {
error(502, 'No se pudieron cargar las canciones.');
}
return { src: 'local' as const, query, tracks: data };
return {
src,
query,
tracks: tracksRes?.data ?? null,
ytResults: searchRes?.data ?? null,
ytError: Boolean(searchRes?.error),
playlists: playlistsRes?.data?.items ?? [],
needsAuth: wantsYouTube && !locals.user
};
};
export const actions: Actions = {