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:
@@ -23,9 +23,15 @@ export function requireUser(locals: App.Locals, url: URL): SessionUser {
|
||||
return locals.user;
|
||||
}
|
||||
|
||||
export function requireAccessToken(locals: App.Locals, message = 'Inicia sesión.'): string {
|
||||
if (!locals.accessToken) error(401, message);
|
||||
return locals.accessToken;
|
||||
export function optionalUser(
|
||||
locals: App.Locals,
|
||||
url: URL,
|
||||
allowAnonymousListening: boolean
|
||||
): SessionUser | null {
|
||||
if (locals.user) return locals.user;
|
||||
if (!allowAnonymousListening)
|
||||
redirect(302, `/login?returnTo=${encodeURIComponent(url.pathname + url.search)}`);
|
||||
return null;
|
||||
}
|
||||
|
||||
export function requireAccessTokenAction(
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createApiClient } from '$lib/server/api';
|
||||
|
||||
const CACHE_TTL_MS = 30_000;
|
||||
|
||||
let cached: { allowAnonymousListening: boolean; expiresAt: number } | null = null;
|
||||
|
||||
export async function getAllowAnonymousListening(fetchFn: typeof fetch): Promise<boolean> {
|
||||
if (cached && cached.expiresAt > Date.now()) return cached.allowAnonymousListening;
|
||||
|
||||
try {
|
||||
const api = createApiClient({ fetch: fetchFn });
|
||||
const { data } = await api.GET('/config/playback');
|
||||
const allowAnonymousListening = data?.allowAnonymousListening ?? false;
|
||||
|
||||
cached = { allowAnonymousListening, expiresAt: Date.now() + CACHE_TTL_MS };
|
||||
return allowAnonymousListening;
|
||||
} catch {
|
||||
// The API being unreachable shouldn't take down every page in the app;
|
||||
// fail closed (as if anonymous listening were off) and retry next time.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -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) };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user