Backend: - New Likes feature (toggle/get liked tracks) and endpoints - User follow/unfollow, listening stats, public user profile - Playlist visibility (public/private) support - Various auth and DTO/response adjustments to support the above Frontend (web-player): - New design system pass: theme tokens/mode (light/dark), glass menu, tabs bar, player dock/extras/queue components, mix tiles, artist link - New routes: Liked, public user profile (/u/[id]), likes API proxy - Search refactor: shared SearchBox component used by TopBar and MobileHeader (search now reachable on mobile), live results as you type instead of requiring Enter, dropped the "Buscando…" quick-result preview - Removed now-unused components (MobileNav, CoverBadge, MixBentoTile, PlayPauseOverlay, SuggestionsTable, userDirectory) - Design handoff reference docs added under web-player/design_handoff_musify_player and web-player/docs/design-system-refactor-plan.md
93 lines
2.6 KiB
TypeScript
93 lines
2.6 KiB
TypeScript
import type { createApiClient } from '$lib/server/api';
|
|
import type { YouTubeAlbumResult, YouTubeSong } from '$lib/types';
|
|
|
|
export const YOUTUBE_FILLER_QUERIES = [
|
|
'pop',
|
|
'rock',
|
|
'reggaeton',
|
|
'lo-fi',
|
|
'indie',
|
|
'salsa',
|
|
'k-pop',
|
|
'electrónica',
|
|
'jazz',
|
|
'baladas',
|
|
'trap',
|
|
'cumbia',
|
|
'hip hop',
|
|
'música clásica'
|
|
];
|
|
|
|
export interface YoutubeFiller {
|
|
query: string;
|
|
items: YouTubeSong[];
|
|
continuationToken: string;
|
|
}
|
|
|
|
export interface YoutubeAlbumFiller {
|
|
query: string;
|
|
items: YouTubeAlbumResult[];
|
|
}
|
|
|
|
function pickRandomFillerQuery(): string {
|
|
return YOUTUBE_FILLER_QUERIES[Math.floor(Math.random() * YOUTUBE_FILLER_QUERIES.length)];
|
|
}
|
|
|
|
function hash(text: string): number {
|
|
let h = 0;
|
|
for (let i = 0; i < text.length; i++) {
|
|
h = (h * 31 + text.charCodeAt(i)) | 0;
|
|
}
|
|
return Math.abs(h);
|
|
}
|
|
|
|
function pickFillerQueryForUser(userId: string, date: Date, variant = 0): string {
|
|
const dayKey = date.toISOString().slice(0, 10);
|
|
const index = (hash(`${userId}:${dayKey}`) + variant) % YOUTUBE_FILLER_QUERIES.length;
|
|
return YOUTUBE_FILLER_QUERIES[index];
|
|
}
|
|
|
|
export async function fetchYoutubeFiller(
|
|
api: ReturnType<typeof createApiClient>,
|
|
accessToken: string | null | undefined,
|
|
limit: number,
|
|
seedQuery?: string,
|
|
userId?: string,
|
|
variant = 0
|
|
): Promise<YoutubeFiller> {
|
|
if (!accessToken) return { query: '', items: [], continuationToken: '' };
|
|
const query =
|
|
seedQuery?.trim() ||
|
|
(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)
|
|
.filter((song): song is NonNullable<typeof song> => song !== null && song !== undefined);
|
|
return {
|
|
query,
|
|
items: (items as YouTubeSong[]).slice(0, limit),
|
|
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) };
|
|
}
|