From 5ae9955bd05e841533dbb3f289dae38ddd363a8f Mon Sep 17 00:00:00 2001 From: XShine555 Date: Tue, 15 Sep 2026 21:17:14 +0200 Subject: [PATCH] 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. --- web-player/.env.example | 11 - web-player/src/lib/albums.ts | 30 +- web-player/src/lib/api/schema.d.ts | 272 +----------------- .../lib/components/AlbumContextMenu.svelte | 15 +- .../lib/components/TrackContextMenu.svelte | 12 +- .../src/lib/components/player/Queue.svelte | 6 +- .../lib/components/player/TrackInfo.svelte | 3 - web-player/src/lib/components/ui/Cover.svelte | 5 +- .../src/lib/components/ui/MixArt.svelte | 14 +- web-player/src/lib/components/ui/Rail.svelte | 29 -- web-player/src/lib/config.ts | 2 - web-player/src/lib/mixes.ts | 24 +- web-player/src/lib/navigation.svelte.ts | 1 - web-player/src/lib/player/liked.svelte.ts | 18 +- web-player/src/lib/player/player.svelte.ts | 93 +----- web-player/src/lib/recentlyPlayed.ts | 4 - web-player/src/lib/server/config.ts | 27 -- web-player/src/lib/server/playlistActions.ts | 70 +---- web-player/src/lib/server/thumbnails.ts | 173 ----------- web-player/src/lib/server/youtube.ts | 92 ------ web-player/src/lib/thumbnails.ts | 19 -- web-player/src/lib/tracks.ts | 73 ++--- web-player/src/lib/types.ts | 18 -- .../routes/(app)/albums/[id]/+page.server.ts | 4 +- .../src/routes/(app)/albums/[id]/+page.svelte | 6 +- .../[id]/components/AlbumTrackTable.svelte | 9 +- .../[source]/[externalId]/+page.server.ts | 32 --- .../[source]/[externalId]/+page.svelte | 179 ------------ .../src/routes/(app)/explore/+page.server.ts | 18 +- .../src/routes/(app)/explore/+page.svelte | 191 ++++-------- .../src/routes/(app)/liked/+page.svelte | 3 - .../routes/(app)/mixes/[id]/+page.server.ts | 5 +- .../src/routes/(app)/mixes/[id]/+page.svelte | 3 - .../routes/(app)/playlists/[id]/+page.svelte | 4 +- .../[id]/components/PlaylistTrackTable.svelte | 25 +- web-player/src/routes/+page.server.ts | 21 +- web-player/src/routes/+page.svelte | 137 +++------ .../[source]/[externalId]/tracks/+server.ts | 12 - .../src/routes/api/thumbnail/+server.ts | 31 -- web-player/src/routes/api/tracks/+server.ts | 3 +- .../[source]/[externalId]/stream/+server.ts | 14 - 41 files changed, 167 insertions(+), 1541 deletions(-) delete mode 100644 web-player/src/lib/components/ui/Rail.svelte delete mode 100644 web-player/src/lib/server/thumbnails.ts delete mode 100644 web-player/src/lib/server/youtube.ts delete mode 100644 web-player/src/lib/thumbnails.ts delete mode 100644 web-player/src/routes/(app)/albums/external/[source]/[externalId]/+page.server.ts delete mode 100644 web-player/src/routes/(app)/albums/external/[source]/[externalId]/+page.svelte delete mode 100644 web-player/src/routes/api/albums/external/[source]/[externalId]/tracks/+server.ts delete mode 100644 web-player/src/routes/api/thumbnail/+server.ts delete mode 100644 web-player/src/routes/api/tracks/external/[source]/[externalId]/stream/+server.ts diff --git a/web-player/.env.example b/web-player/.env.example index 8efa0a2..75b940b 100644 --- a/web-player/.env.example +++ b/web-player/.env.example @@ -26,17 +26,6 @@ API_BASE_URL=http://localhost:5111 # Dev already allows a plain-HTTP issuer. Set this only to allow it in a production build (never with a public issuer). # AUTH_ALLOW_INSECURE_HTTP=true -# Optional tuning for the YouTube artwork proxy (/api/youtube/thumbnail), -# which caches the covers in memory so the browser never hits Google directly. -# THUMBNAIL_CACHE_MAX_AGE_SECONDS=2592000 -# THUMBNAIL_FAILURE_CACHE_SECONDS=300 -# THUMBNAIL_CACHE_BYTES=134217728 -# THUMBNAIL_MAX_CONCURRENT_FETCHES=4 -# THUMBNAIL_FETCH_TIMEOUT_MS=8000 -# THUMBNAIL_COOLDOWN_SECONDS=30 -# THUMBNAIL_MAX_COOLDOWN_SECONDS=300 -# THUMBNAIL_MAX_SIZE=720 - # Where to send presigned S3 PUTs when the public S3 host is not reachable from # the server (in Docker: http://seaweedfs:8333). The signed Host header is kept # either way. Defaults to the host in the presigned URL. diff --git a/web-player/src/lib/albums.ts b/web-player/src/lib/albums.ts index 1ce1b86..9d85015 100644 --- a/web-player/src/lib/albums.ts +++ b/web-player/src/lib/albums.ts @@ -1,30 +1,8 @@ import { toQueueItems, type ApiTrackLike, type QueueItem } from '$lib/player/player.svelte'; -interface YouTubeAlbumTracksResponse { - album: { artist: string; thumbnailUrl: string }; - tracks: { videoId: string; title: string; isExplicit: boolean }[]; -} - -export async function fetchAlbumQueueItems( - kind: 'local' | 'youtube', - albumId: string -): Promise { - if (kind === 'local') { - const res = await fetch(`/api/albums/${albumId}/tracks`); - if (!res.ok) return []; - const data = (await res.json()) as { items: ApiTrackLike[] }; - return toQueueItems(data.items ?? []); - } - - const res = await fetch(`/api/albums/external/${kind}/${albumId}/tracks`); +export async function fetchAlbumQueueItems(albumId: string): Promise { + const res = await fetch(`/api/albums/${albumId}/tracks`); if (!res.ok) return []; - const detail = (await res.json()) as YouTubeAlbumTracksResponse; - return detail.tracks.map((track) => ({ - id: track.videoId, - title: track.title, - artist: detail.album.artist, - source: 'youtube' as const, - coverUrl: detail.album.thumbnailUrl, - explicit: track.isExplicit - })); + const data = (await res.json()) as { items: ApiTrackLike[] }; + return toQueueItems(data.items ?? []); } diff --git a/web-player/src/lib/api/schema.d.ts b/web-player/src/lib/api/schema.d.ts index bccb1cb..8d19371 100644 --- a/web-player/src/lib/api/schema.d.ts +++ b/web-player/src/lib/api/schema.d.ts @@ -11,7 +11,7 @@ export interface paths { path?: never; cookie?: never; }; - /** Get Paginated Albums, Combined With Live YouTube Music Results When A Title Filter Is Given. */ + /** Get Paginated Albums, Optionally Filtered By Title. */ get: operations["GetAlbums"]; put?: never; /** Create A New Album. */ @@ -39,23 +39,6 @@ export interface paths { patch?: never; trace?: never; }; - "/albums/external/{source}/{externalId}": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Get An Album From An External Source (Currently Only YouTube Music) With Its Tracklist. */ - get: operations["GetExternalAlbum"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/albums/users/{userId}": { parameters: { query?: never; @@ -220,7 +203,7 @@ export interface paths { }; get?: never; put?: never; - /** Like Or Unlike A Track, By Id Or By External Source, Provisioning It In The Background If Needed. */ + /** Like Or Unlike A Track By Id. */ post: operations["ToggleTrackLike"]; delete?: never; options?: never; @@ -324,7 +307,7 @@ export interface paths { /** Get Paginated Tracks Of A PlayList. */ get: operations["GetPlayListTracks"]; put?: never; - /** Add A Track To A PlayList, By Id Or By External Source, Provisioning It In The Background If Needed. */ + /** Add One Of Your Tracks To A PlayList. */ post: operations["AddPlayListTrack"]; delete?: never; options?: never; @@ -408,7 +391,7 @@ export interface paths { path?: never; cookie?: never; }; - /** Get Paginated Tracks, Combined With Live YouTube Music Results When A Name Filter Is Given. */ + /** Get Paginated Tracks, Optionally Filtered By Name. */ get: operations["GetTracks"]; put?: never; /** Create A New Track. */ @@ -470,23 +453,6 @@ export interface paths { patch?: never; trace?: never; }; - "/tracks/external/{source}/{externalId}/stream": { - parameters: { - query?: never; - header?: never; - path?: never; - cookie?: never; - }; - /** Resolve Playback For A Track From An External Source (Currently Only YouTube Music): Server Stream If Downloaded, Direct Stream From The Source Otherwise. Works Anonymously When The Playback Configuration Allows It. */ - get: operations["GetExternalTrackStream"]; - put?: never; - post?: never; - delete?: never; - options?: never; - head?: never; - patch?: never; - trace?: never; - }; "/tracks/users/{userId}": { parameters: { query?: never; @@ -665,37 +631,9 @@ export interface components { schemas: { AddPlayListTrackRequest: { /** Format: uuid */ - trackId: null | string; - source: null | components["schemas"]["TrackSource"]; - externalId: null | string; + trackId: string; }; - AlbumApplicationResponse: components["schemas"]["AlbumApplicationResponseAlbumApplicationResponse"] | components["schemas"]["AlbumApplicationResponseExternalAlbumApplicationResponse"]; - AlbumApplicationResponseAlbumApplicationResponse: { - /** @enum {string} */ - source?: "Local"; - /** Format: uuid */ - id: string; - title: string; - description: null | string; - /** Format: int32 */ - releaseYear: null | number | string; - ownerUserId: string; - /** Format: int32 */ - trackCount: number | string; - smallImageKeyName: null | string; - mediumImageKeyName: null | string; - largeImageKeyName: null | string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; - coverTrackIds: string[]; - }; - AlbumApplicationResponseExternalAlbumApplicationResponse: { - /** @enum {string} */ - source?: "YouTube"; - externalId: string; - thumbnailUrl: null | string; + AlbumApplicationResponse: { /** Format: uuid */ id: string; title: string; @@ -726,9 +664,7 @@ export interface components { uploadUrl: string; }; AlbumSearchItemResponse: { - source: components["schemas"]["TrackSource"]; - album: null | components["schemas"]["AlbumApplicationResponse"]; - youTubeAlbum: null | components["schemas"]["YouTubeAlbumResult"]; + album: components["schemas"]["AlbumApplicationResponse"]; }; AlbumsSearchResponse: { items: components["schemas"]["AlbumSearchItemResponse"][]; @@ -741,9 +677,6 @@ export interface components { /** Format: int32 */ totalItemCount: number | string; hasPreviousPage: boolean; - hasNextLocalPage: boolean; - nextYoutubeContinuationToken: null | string; - youtubeUnavailable: boolean; hasNextPage: boolean; }; CreateAlbumRequest: { @@ -804,19 +737,13 @@ export interface components { items: components["schemas"]["MixItemApplicationResponse"][]; }; MixItemApplicationResponse: { - source: components["schemas"]["MixItemSource"]; /** Format: uuid */ - trackId: null | string; - videoId: null | string; + trackId: string; title: string; artist: null | string; - thumbnailUrl: null | string; /** Format: double */ durationSeconds: number | string; - isExplicit: boolean; }; - /** @enum {unknown} */ - MixItemSource: "Musify" | "YouTube"; PaginatedResponseOfAlbumApplicationResponse: { items: components["schemas"]["AlbumApplicationResponse"][]; /** Format: int32 */ @@ -928,15 +855,9 @@ export interface components { }; ToggleLikeRequest: { /** Format: uuid */ - trackId: null | string; - source: null | components["schemas"]["TrackSource"]; - externalId: null | string; + trackId: string; }; - TrackApplicationResponse: components["schemas"]["TrackApplicationResponseLocalTrackApplicationResponse"] | components["schemas"]["TrackApplicationResponseExternalTrackApplicationResponse"]; - TrackApplicationResponseExternalTrackApplicationResponse: { - /** @enum {string} */ - source?: "YouTube"; - externalId: string; + TrackApplicationResponse: { /** Format: uuid */ id: string; title: string; @@ -950,32 +871,11 @@ export interface components { createdAt: string; /** Format: date-time */ updatedAt: string; - }; - TrackApplicationResponseLocalTrackApplicationResponse: { - /** @enum {string} */ - source?: "Local"; ownerUserId: string; - /** Format: uuid */ - id: string; - title: string; - artist: null | string; - audioStatus: components["schemas"]["ProcessingStatus"]; - /** Format: double */ - duration: number | string; - /** Format: int32 */ - listensCount: number | string; - /** Format: date-time */ - createdAt: string; - /** Format: date-time */ - updatedAt: string; }; TrackSearchItemResponse: { - source: components["schemas"]["TrackSource"]; - track: null | components["schemas"]["TrackApplicationResponse"]; - youTubeSong: null | components["schemas"]["YouTubeSongResult"]; + track: components["schemas"]["TrackApplicationResponse"]; }; - /** @enum {unknown} */ - TrackSource: "Local" | "YouTube"; TracksSearchResponse: { items: components["schemas"]["TrackSearchItemResponse"][]; /** Format: int32 */ @@ -987,9 +887,6 @@ export interface components { /** Format: int32 */ totalItemCount: number | string; hasPreviousPage: boolean; - hasNextLocalPage: boolean; - nextYoutubeContinuationToken: null | string; - youtubeUnavailable: boolean; hasNextPage: boolean; }; TrackStreamResponse: { @@ -1054,59 +951,6 @@ export interface components { /** Format: int32 */ followingCount: number | string; }; - YouTubeAlbumDetail: { - album: components["schemas"]["YouTubeAlbumResult"]; - description: string; - /** Format: int32 */ - totalDurationSeconds: number | string; - tracks: components["schemas"]["YouTubeAlbumTrack"][]; - }; - YouTubeAlbumResult: { - albumId: string; - title: string; - artist: string; - thumbnailUrl: string; - /** Format: int32 */ - releaseYear: null | number | string; - isSingle: boolean; - isEp: boolean; - artists: components["schemas"]["YouTubeArtistRef"][]; - }; - YouTubeAlbumTrack: { - videoId: string; - title: string; - /** Format: int32 */ - durationSeconds: number | string; - /** Format: int32 */ - trackNumber: number | string; - isExplicit: boolean; - }; - YouTubeArtistRef: { - id: null | string; - name: string; - }; - YouTubeSongResult: { - videoId: string; - title: string; - artist: string; - album: string; - /** Format: int32 */ - durationSeconds: number | string; - thumbnailUrl: string; - isExplicit: boolean; - artists: components["schemas"]["YouTubeArtistRef"][]; - }; - /** @enum {unknown} */ - YouTubeStreamMode: "Server" | "YouTube"; - YouTubeStreamResponse: { - /** Format: uuid */ - trackId: null | string; - mode: components["schemas"]["YouTubeStreamMode"]; - streamUrl: string; - ticket: null | string; - /** Format: int32 */ - expiresInSeconds: number | string; - }; }; responses: never; parameters: never; @@ -1122,7 +966,6 @@ export interface operations { title?: string; pageNumber?: number | string; pageSize?: number | string; - youtubeContinuationToken?: string; }; header?: never; path?: never; @@ -1217,36 +1060,6 @@ export interface operations { }; }; }; - GetExternalAlbum: { - parameters: { - query?: never; - header?: never; - path: { - source: string; - externalId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["YouTubeAlbumDetail"]; - }; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; GetAlbumsByUserId: { parameters: { query?: { @@ -1648,15 +1461,6 @@ export interface operations { "application/json": boolean; }; }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; - }; - }; /** @description Unauthorized */ 401: { headers: { @@ -1918,15 +1722,6 @@ export interface operations { "application/json": components["schemas"]["TrackApplicationResponse"]; }; }; - /** @description Bad Request */ - 400: { - headers: { - [name: string]: unknown; - }; - content: { - "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; - }; - }; /** @description Unauthorized */ 401: { headers: { @@ -2150,7 +1945,6 @@ export interface operations { name?: string; pageNumber?: number | string; pageSize?: number | string; - youtubeContinuationToken?: string; }; header?: never; path?: never; @@ -2317,50 +2111,6 @@ export interface operations { }; }; }; - GetExternalTrackStream: { - parameters: { - query?: never; - header?: never; - path: { - source: string; - externalId: string; - }; - cookie?: never; - }; - requestBody?: never; - responses: { - /** @description OK */ - 200: { - headers: { - [name: string]: unknown; - }; - content: { - "application/json": components["schemas"]["YouTubeStreamResponse"]; - }; - }; - /** @description Unauthorized */ - 401: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Not Found */ - 404: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - /** @description Conflict */ - 409: { - headers: { - [name: string]: unknown; - }; - content?: never; - }; - }; - }; GetTracksByUserId: { parameters: { query?: { diff --git a/web-player/src/lib/components/AlbumContextMenu.svelte b/web-player/src/lib/components/AlbumContextMenu.svelte index 1249bd4..b7a769e 100644 --- a/web-player/src/lib/components/AlbumContextMenu.svelte +++ b/web-player/src/lib/components/AlbumContextMenu.svelte @@ -2,18 +2,11 @@ const MENU_WIDTH = 240; const SUBMENU_WIDTH = 256; - export type AlbumMenuState = { x: number; y: number; openLeft: boolean } & ( - { kind: 'local'; albumId: string } | { kind: 'youtube'; albumId: string } - ); + export type AlbumMenuState = { x: number; y: number; openLeft: boolean; albumId: string }; - export function albumContextMenuStateFor( - event: MouseEvent, - kind: 'local' | 'youtube', - albumId: string - ): AlbumMenuState { + export function albumContextMenuStateFor(event: MouseEvent, albumId: string): AlbumMenuState { event.preventDefault(); return { - kind, albumId, x: Math.max(8, Math.min(event.clientX, window.innerWidth - MENU_WIDTH - 8)), y: Math.max(8, Math.min(event.clientY, window.innerHeight - 60)), @@ -93,9 +86,7 @@ {#each playlists as playlist (playlist.id)}
({ update }) => { onClose(); diff --git a/web-player/src/lib/components/TrackContextMenu.svelte b/web-player/src/lib/components/TrackContextMenu.svelte index 2e569a9..06f9807 100644 --- a/web-player/src/lib/components/TrackContextMenu.svelte +++ b/web-player/src/lib/components/TrackContextMenu.svelte @@ -78,7 +78,7 @@ {#each playlists as playlist (playlist.id)} ({ update }) => { onClose(); @@ -86,15 +86,7 @@ }} > - {#if menu.kind === 'youtube'} - - - - - - {:else} - - {/if} + - {/snippet} - - - {#if form?.message} - {form.message} - {/if} - - {#if data.album.description} -

{data.album.description}

- {/if} - - - {#each tracks as track, i (track.videoId)} - {@const active = player.current.id === track.videoId} - (contextMenu = contextMenuStateFor(e, targetFor(i)))} - > - playFrom(i)} /> - playFrom(i)} - /> - {fmtTime(Number(track.durationSeconds))} - - {/each} - - - -{#if contextMenu} - (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} - (albumMenu = null)} - onPlayNext={() => { - player.playNext(queue); - albumMenu = null; - }} - onAddToQueue={() => { - player.appendToQueue(queue); - albumMenu = null; - }} - /> -{/if} diff --git a/web-player/src/routes/(app)/explore/+page.server.ts b/web-player/src/routes/(app)/explore/+page.server.ts index 3556497..00cfb0e 100644 --- a/web-player/src/routes/(app)/explore/+page.server.ts +++ b/web-player/src/routes/(app)/explore/+page.server.ts @@ -1,12 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; import { createApiClient, unwrapOrError } from '$lib/server/api'; -import { YOUTUBE_FILLER_QUERIES } from '$lib/server/youtube'; -import { - addAlbumToPlaylistAction, - addTrackAction, - addYouTubeToPlaylistAction, - addYoutubeAlbumToPlaylistAction -} from '$lib/server/playlistActions'; +import { addAlbumToPlaylistAction, addTrackAction } from '$lib/server/playlistActions'; import { EXPLORE_ALBUMS_PAGE_SIZE, EXPLORE_PAGE_SIZE as PAGE_SIZE, @@ -53,22 +47,16 @@ export const load: PageServerLoad = async ({ url, locals, fetch }) => { const tracks = unwrapOrError(tracksRes, 'No se pudieron cargar las canciones.'); - const albumItems = albumsRes?.data?.items ?? []; - return { query, tracks, - albums: albumItems.flatMap((item) => (item.album ? [item.album] : [])), - youtubeAlbums: albumItems.flatMap((item) => (item.youTubeAlbum ? [item.youTubeAlbum] : [])), + albums: (albumsRes?.data?.items ?? []).map((item) => item.album), users: usersRes?.data?.items ?? [], - genres: YOUTUBE_FILLER_QUERIES, playlists: playlistsRes?.data?.items ?? [] }; }; export const actions: Actions = { addTrack: addTrackAction, - addYouTubeToPlaylist: addYouTubeToPlaylistAction, - addAlbumToPlaylist: addAlbumToPlaylistAction, - addYoutubeAlbumToPlaylist: addYoutubeAlbumToPlaylistAction + addAlbumToPlaylist: addAlbumToPlaylistAction }; diff --git a/web-player/src/routes/(app)/explore/+page.svelte b/web-player/src/routes/(app)/explore/+page.svelte index 6c335ab..ea8c60b 100644 --- a/web-player/src/routes/(app)/explore/+page.svelte +++ b/web-player/src/routes/(app)/explore/+page.svelte @@ -14,8 +14,7 @@ import NowPlaying from '$lib/components/ui/NowPlaying.svelte'; import SearchResultRow from '$lib/components/ui/SearchResultRow.svelte'; import SectionHeading from '$lib/components/ui/SectionHeading.svelte'; - import { EXPLORE_ALBUMS_LIMIT, EXPLORE_PAGE_SIZE } from '$lib/config'; - import type { YouTubeSong } from '$lib/types'; + import { EXPLORE_PAGE_SIZE } from '$lib/config'; import TrackContextMenu, { contextMenuStateFor, type ContextMenuState @@ -29,7 +28,6 @@ isTargetCurrent, queueItemForTarget, targetArtist, - targetCoverSrc, targetExplicit, targetId, targetTitle, @@ -43,6 +41,8 @@ user: 'Usuario' }; + // No hay sistema de tags/géneros todavía: esta lista es un placeholder de + // cliente y cada tile enlaza a una búsqueda normal por ese término. const GENRE_INFO: Record = { pop: { label: 'Pop', tagline: 'Éxitos que suenan en todas partes' }, rock: { label: 'Rock', tagline: 'Guitarras, actitud y ruido' }, @@ -60,23 +60,26 @@ 'música clásica': { label: 'Clásica', tagline: 'Siglos de composición' } }; + const genreTiles = Object.entries(GENRE_INFO).map(([query, info], i, all) => ({ + query, + hue: Math.round((i * 360) / all.length), + ...info + })); + let { data, form } = $props(); const SEARCH_FILTERS = ['Todo', 'Canciones', 'Álbumes', 'Playlists', 'Usuarios'] as const; type SearchFilter = (typeof SEARCH_FILTERS)[number]; const SEARCH_GROUP_PREVIEW = 4; - let ytItems = $state([]); - let ytUnavailable = $state(false); let contextMenu = $state(null); let albumMenu = $state(null); let sfilter = $state('Todo'); - type LocalTrack = NonNullable<(typeof data.tracks.items)[number]['track']>; + type LocalTrack = (typeof data.tracks.items)[number]['track']; let localItems = $state([]); let localPage = $state(1); - let ytContinuation = $state(''); let hasMore = $state(false); let loadingMore = $state(false); @@ -84,63 +87,24 @@ contextMenu = null; albumMenu = null; sfilter = 'Todo'; - localItems = data.tracks.items.flatMap((item) => (item.track ? [item.track] : [])); + localItems = data.tracks.items.map((item) => item.track); localPage = Number(data.tracks.pageNumber); - ytContinuation = data.tracks.nextYoutubeContinuationToken ?? ''; hasMore = data.tracks.hasNextPage; - - if (data.query) { - ytItems = appendUnique( - [], - data.tracks.items.flatMap((item) => - item.youTubeSong ? [item.youTubeSong as YouTubeSong] : [] - ), - (i) => i.videoId - ); - ytUnavailable = data.tracks.youtubeUnavailable; - } else { - ytItems = []; - ytUnavailable = false; - } }); - const genreTiles = $derived( - data.genres.map((query, i) => ({ - query, - hue: Math.round((i * 360) / data.genres.length), - ...(GENRE_INFO[query] ?? { label: query, tagline: 'Explora este estilo' }) + const songRows = $derived( + localItems.map((track) => ({ + target: { track } satisfies TrackTarget, + seconds: Number(track.duration) })) ); - const songRows = $derived([ - ...localItems.map((track) => ({ - target: { kind: 'local' as const, track }, - seconds: Number(track.duration) - })), - ...ytItems.map((song) => ({ - target: { kind: 'youtube' as const, song }, - seconds: Number(song.durationSeconds) - })) - ] satisfies { target: TrackTarget; seconds: number }[]); - const items = $derived(songRows.map((row) => row.target)); const albums = $derived(data.albums); - const youtubeAlbums = $derived(data.youtubeAlbums); const users = $derived(data.users); - type LocalAlbum = (typeof data.albums)[number]; - type YouTubeAlbum = (typeof data.youtubeAlbums)[number]; - type AlbumEntry = { kind: 'local'; album: LocalAlbum } | { kind: 'youtube'; album: YouTubeAlbum }; - - const albumEntries = $derived( - [ - ...albums.map((album) => ({ kind: 'local' as const, album })), - ...youtubeAlbums.map((album) => ({ kind: 'youtube' as const, album })) - ].slice(0, EXPLORE_ALBUMS_LIMIT) - ); - - const hasAlbums = $derived(albumEntries.length > 0); + const hasAlbums = $derived(albums.length > 0); const hasUsers = $derived(users.length > 0); const playlistMatches = $derived( @@ -151,12 +115,12 @@ const hasPlaylists = $derived(playlistMatches.length > 0); const nothingFound = $derived( - !!data.query && !ytUnavailable && items.length === 0 && !hasAlbums && !hasUsers && !hasPlaylists + !!data.query && items.length === 0 && !hasAlbums && !hasUsers && !hasPlaylists ); const searchCounts = $derived({ Canciones: songRows.length, - Álbumes: albumEntries.length, + Álbumes: albums.length, Playlists: playlistMatches.length, Usuarios: users.length }); @@ -172,7 +136,7 @@ type TopResult = | { kind: 'track'; target: TrackTarget } - | { kind: 'album'; entry: AlbumEntry } + | { kind: 'album'; album: (typeof albums)[number] } | { kind: 'playlist'; playlist: (typeof playlistMatches)[number] } | { kind: 'user'; user: (typeof users)[number] }; @@ -185,14 +149,14 @@ if (userHit) return { kind: 'user', user: userHit }; const trackHit = items.find((target) => startsWithQuery(targetTitle(target))); if (trackHit) return { kind: 'track', target: trackHit }; - const albumHit = albumEntries.find((entry) => startsWithQuery(entry.album.title)); - if (albumHit) return { kind: 'album', entry: albumHit }; + const albumHit = albums.find((album) => startsWithQuery(album.title)); + if (albumHit) return { kind: 'album', album: albumHit }; const playlistHit = playlistMatches.find((p) => startsWithQuery(p.name)); if (playlistHit) return { kind: 'playlist', playlist: playlistHit }; if (users.length) return { kind: 'user', user: users[0] }; if (items.length) return { kind: 'track', target: items[0] }; - if (albumEntries.length) return { kind: 'album', entry: albumEntries[0] }; + if (albums.length) return { kind: 'album', album: albums[0] }; if (playlistMatches.length) return { kind: 'playlist', playlist: playlistMatches[0] }; return null; }); @@ -216,11 +180,7 @@ const topResultHref = $derived.by(() => { if (!topResult || topResult.kind === 'track') return undefined; - if (topResult.kind === 'album') { - return topResult.entry.kind === 'local' - ? `/albums/${topResult.entry.album.id}` - : `/albums/external/youtube/${topResult.entry.album.albumId}`; - } + if (topResult.kind === 'album') return `/albums/${topResult.album.id}`; if (topResult.kind === 'playlist') return `/playlists/${topResult.playlist.id}`; return `/u/${topResult.user.id}`; }); @@ -233,22 +193,22 @@ contextMenu = contextMenuStateFor(event, target); } - function openAlbumMenu(event: MouseEvent, kind: 'local' | 'youtube', albumId: string) { - albumMenu = albumContextMenuStateFor(event, kind, albumId); + function openAlbumMenu(event: MouseEvent, albumId: string) { + albumMenu = albumContextMenuStateFor(event, albumId); } async function albumPlayNext() { if (!albumMenu) return; - const { kind, albumId } = albumMenu; + const { albumId } = albumMenu; albumMenu = null; - player.playNext(await fetchAlbumQueueItems(kind, albumId)); + player.playNext(await fetchAlbumQueueItems(albumId)); } async function albumAddToQueue() { if (!albumMenu) return; - const { kind, albumId } = albumMenu; + const { albumId } = albumMenu; albumMenu = null; - player.appendToQueue(await fetchAlbumQueueItems(kind, albumId)); + player.appendToQueue(await fetchAlbumQueueItems(albumId)); } function addToQueue() { @@ -265,25 +225,16 @@ pageNumber: String(localPage + 1), pageSize: String(EXPLORE_PAGE_SIZE) }); - if (data.query) { - params.set('name', data.query); - if (ytContinuation) params.set('youtubeContinuationToken', ytContinuation); - } + if (data.query) params.set('name', data.query); const res = await fetch(`/api/tracks?${params}`); if (!res.ok) throw new Error(String(res.status)); const next = (await res.json()) as typeof data.tracks; localItems = appendUnique( localItems, - next.items.flatMap((item) => (item.track ? [item.track] : [])), + next.items.map((item) => item.track), (track) => track.id ); - ytItems = appendUnique( - ytItems, - next.items.flatMap((item) => (item.youTubeSong ? [item.youTubeSong as YouTubeSong] : [])), - (song) => song.videoId - ); localPage = Number(next.pageNumber); - ytContinuation = next.nextYoutubeContinuationToken ?? ''; hasMore = next.hasNextPage; } catch { hasMore = false; @@ -346,26 +297,15 @@ {#if topResult.kind === 'track'} {:else if topResult.kind === 'album'} - {#if topResult.entry.kind === 'local'} - - {:else} - - {/if} + {:else if topResult.kind === 'playlist'} {targetArtist(topResult.target)} - {:else if topResult.kind === 'album' && topResult.entry.kind === 'youtube' && topResult.entry.album.artist} -
- {topResult.entry.album.artist} -
{/if} @@ -517,7 +453,6 @@ {#snippet art(artClass)} {/if} - {:else if !ytUnavailable} + {:else}
    - {#each capped(albumEntries) as entry (entry.kind === 'local' ? entry.album.id : entry.album.albumId)} + {#each capped(albums) as album (album.id)}
  • - {#if entry.kind === 'local'} - openAlbumMenu(e, 'local', entry.album.id)} - > - {#snippet art(artClass)} - - {/snippet} - - {:else} - openAlbumMenu(e, 'youtube', entry.album.albumId)} - > - {#snippet art(artClass)} - - {/snippet} - - {/if} + openAlbumMenu(e, album.id)} + > + {#snippet art(artClass)} + + {/snippet} +
  • {/each}
@@ -665,13 +579,6 @@ {/if} {/if} - - {#if ytUnavailable} - - {/if} {#if contextMenu} diff --git a/web-player/src/routes/(app)/liked/+page.svelte b/web-player/src/routes/(app)/liked/+page.svelte index dc9d6ad..1184d01 100644 --- a/web-player/src/routes/(app)/liked/+page.svelte +++ b/web-player/src/routes/(app)/liked/+page.svelte @@ -23,8 +23,6 @@ id: t.id, title: t.title, artist: t.artist, - source: t.source, - coverUrl: t.coverUrl, explicit: t.explicit })) ); @@ -97,7 +95,6 @@ title={track.title} artist={track.artist} ownerUserId={track.ownerUserId} - coverSrc={track.coverUrl} explicit={track.explicit} {active} onClick={() => playFrom(i)} diff --git a/web-player/src/routes/(app)/mixes/[id]/+page.server.ts b/web-player/src/routes/(app)/mixes/[id]/+page.server.ts index 2243490..5312412 100644 --- a/web-player/src/routes/(app)/mixes/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/mixes/[id]/+page.server.ts @@ -1,6 +1,6 @@ import type { PageServerLoad, Actions } from './$types'; import { createApiClient, requireUser, unwrapOrError } from '$lib/server/api'; -import { addTrackAction, addYouTubeToPlaylistAction } from '$lib/server/playlistActions'; +import { addTrackAction } from '$lib/server/playlistActions'; import { PLAYLIST_PICKER_PAGE_SIZE } from '$lib/config'; export const load: PageServerLoad = async ({ params, locals, url, fetch }) => { @@ -23,6 +23,5 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => { }; export const actions: Actions = { - addTrack: addTrackAction, - addYouTubeToPlaylist: addYouTubeToPlaylistAction + addTrack: addTrackAction }; diff --git a/web-player/src/routes/(app)/mixes/[id]/+page.svelte b/web-player/src/routes/(app)/mixes/[id]/+page.svelte index 44dffc2..f129525 100644 --- a/web-player/src/routes/(app)/mixes/[id]/+page.svelte +++ b/web-player/src/routes/(app)/mixes/[id]/+page.svelte @@ -25,7 +25,6 @@ isTargetCurrent, queueItemForTarget, targetArtist, - targetCoverSrc, targetId, targetTitle } from '$lib/tracks'; @@ -107,8 +106,6 @@ trackId={targetId(targets[i])} title={targetTitle(targets[i])} artist={targetArtist(targets[i])} - coverSrc={targetCoverSrc(targets[i])} - explicit={item.isExplicit} {active} onClick={() => playFrom(i)} /> diff --git a/web-player/src/routes/(app)/playlists/[id]/+page.svelte b/web-player/src/routes/(app)/playlists/[id]/+page.svelte index 9ec5f94..abb8846 100644 --- a/web-player/src/routes/(app)/playlists/[id]/+page.svelte +++ b/web-player/src/routes/(app)/playlists/[id]/+page.svelte @@ -1,6 +1,6 @@