Add anonymous listening support and redesign web-player UI

Backend:
- Expose GET /config/playback so the frontend can detect whether
  anonymous listening is enabled without requiring auth
- Wire anonymous access through track streaming and YouTube resolve flows

Frontend:
- Allow browsing/listening without a session when the backend enables it
- Redesign Inicio, Sidebar and top bar with a new dark theme, typography,
  and layout (top bar with live quick-search, sticky/blurred on scroll)
- Populate Top música, Popular and Recomendados with real Musify +
  YouTube Music data instead of placeholders
- Various shape, spacing, color and animation refinements across the
  redesigned components
This commit is contained in:
2026-09-11 01:29:37 +02:00
parent 10766bbb47
commit 389b3b9244
40 changed files with 1172 additions and 452 deletions
+32 -5
View File
@@ -1,5 +1,5 @@
import type { createApiClient } from '$lib/server/api';
import type { YouTubeSong } from '$lib/types';
import type { YouTubeAlbumResult, YouTubeSong } from '$lib/types';
const YOUTUBE_FILLER_QUERIES = [
'pop',
@@ -24,6 +24,11 @@ export interface YoutubeFiller {
continuationToken: string;
}
export interface YoutubeAlbumFiller {
query: string;
items: YouTubeAlbumResult[];
}
function pickRandomFillerQuery(): string {
return YOUTUBE_FILLER_QUERIES[Math.floor(Math.random() * YOUTUBE_FILLER_QUERIES.length)];
}
@@ -36,9 +41,9 @@ function hash(text: string): number {
return Math.abs(h);
}
function pickFillerQueryForUser(userId: string, date: Date): string {
function pickFillerQueryForUser(userId: string, date: Date, variant = 0): string {
const dayKey = date.toISOString().slice(0, 10);
const index = hash(`${userId}:${dayKey}`) % YOUTUBE_FILLER_QUERIES.length;
const index = (hash(`${userId}:${dayKey}`) + variant) % YOUTUBE_FILLER_QUERIES.length;
return YOUTUBE_FILLER_QUERIES[index];
}
@@ -47,12 +52,13 @@ export async function fetchYoutubeFiller(
accessToken: string | null | undefined,
limit: number,
seedQuery?: string,
userId?: string
userId?: string,
variant = 0
): Promise<YoutubeFiller> {
if (!accessToken) return { query: '', items: [], continuationToken: '' };
const query =
seedQuery?.trim() ||
(userId ? pickFillerQueryForUser(userId, new Date()) : pickRandomFillerQuery());
(userId ? pickFillerQueryForUser(userId, new Date(), variant) : pickRandomFillerQuery());
const { data } = await api.GET('/tracks', { params: { query: { name: query, pageSize: 1 } } });
const items = (data?.items ?? [])
.map((item) => item.youTubeSong)
@@ -63,3 +69,24 @@ export async function fetchYoutubeFiller(
continuationToken: data?.nextYoutubeContinuationToken ?? ''
};
}
export async function fetchYoutubeAlbumFiller(
api: ReturnType<typeof createApiClient>,
accessToken: string | null | undefined,
limit: number,
seedQuery?: string,
userId?: string,
variant = 0
): Promise<YoutubeAlbumFiller> {
if (!accessToken) return { query: '', items: [] };
const query =
seedQuery?.trim() ||
(userId ? pickFillerQueryForUser(userId, new Date(), variant) : pickRandomFillerQuery());
const { data } = await api.GET('/albums', {
params: { query: { title: query, pageNumber: 1, pageSize: limit + 5 } }
});
const items = (data?.items ?? [])
.map((item) => item.youTubeAlbum)
.filter((album): album is NonNullable<typeof album> => album !== null && album !== undefined);
return { query, items: (items as YouTubeAlbumResult[]).slice(0, limit) };
}