From 27f3c02915bc710932a56e72f2bf66ff40fa9240 Mon Sep 17 00:00:00 2001 From: XShine555 Date: Mon, 20 Jul 2026 20:20:34 +0200 Subject: [PATCH] fix(web-player): follow the API types where they are nullable The checked-in schema was stale and typed artist, description and externalId as always present, so the code trusted values the API returns as null. The regenerated schema tells the truth; the API-shaped types now admit null and it is normalised on the way into the player, where the state stays non-null. --- web-player/src/lib/api/schema.d.ts | 493 +++++++++++++++++- .../src/lib/components/TrackTile.svelte | 4 +- web-player/src/lib/components/ui/Cover.svelte | 2 +- web-player/src/lib/components/ui/Input.svelte | 8 +- .../src/lib/components/ui/PlaylistCard.svelte | 2 +- .../lib/components/ui/TrackTitleCell.svelte | 4 +- web-player/src/lib/player/player.svelte.ts | 6 +- web-player/src/lib/tracks.ts | 2 +- .../(app)/playlists/[id]/+page.server.ts | 2 +- .../routes/(app)/playlists/[id]/+page.svelte | 2 +- .../[id]/components/PlaylistHeader.svelte | 2 +- .../[id]/components/SuggestionsTable.svelte | 2 +- 12 files changed, 505 insertions(+), 24 deletions(-) diff --git a/web-player/src/lib/api/schema.d.ts b/web-player/src/lib/api/schema.d.ts index a1e37f0..00f3fc3 100644 --- a/web-player/src/lib/api/schema.d.ts +++ b/web-player/src/lib/api/schema.d.ts @@ -4,6 +4,111 @@ */ export interface paths { + "/albums": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Paginated Albums. */ + get: operations["GetAlbums"]; + put?: never; + /** Create A New Album. */ + post: operations["CreateAlbum"]; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/albums/{id}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get An Album By Id. */ + get: operations["GetAlbumById"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/albums/users/{userId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get Paginated Albums For A User. */ + get: operations["GetAlbumsByUserId"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/albums/{albumId}/tracks": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** Get The Tracks Of An Album Ordered By Track Number. */ + get: operations["GetAlbumTracks"]; + put?: never; + post?: never; + delete?: never; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/albums/{albumId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + /** Update An Album. */ + put: operations["UpdateAlbum"]; + post?: never; + /** Delete An Album. */ + delete: operations["DeleteAlbum"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; + "/albums/{albumId}/tracks/{trackId}": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + get?: never; + put?: never; + /** Add One Of Your Uploaded Tracks To An Album. */ + post: operations["AddTrackToAlbum"]; + /** Remove A Track From An Album. */ + delete: operations["RemoveTrackFromAlbum"]; + options?: never; + head?: never; + patch?: never; + trace?: never; + }; "/playlists": { parameters: { query?: never; @@ -372,11 +477,31 @@ export interface components { schemas: { AddYouTubeTrackRequest: { videoId: string; + }; + AlbumApplicationResponse: { + /** Format: uuid */ + id: string; title: string; - artist: string; + description: null | string; /** Format: int32 */ - durationSeconds: number | string; - thumbnailUrl: string; + releaseYear: null | number | string; + /** Format: int64 */ + ownerUserId: number | 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; + }; + CreateAlbumRequest: { + title: string; + description: null | string; + /** Format: int32 */ + releaseYear: null | number | string; }; CreatePlayListRequest: { name: string; @@ -409,6 +534,19 @@ export interface components { [key: string]: string[]; }; }; + PaginatedResponseOfAlbumApplicationResponse: { + items: components["schemas"]["AlbumApplicationResponse"][]; + /** Format: int32 */ + pageNumber: number | string; + /** Format: int32 */ + pageSize: number | string; + /** Format: int32 */ + pageCount: number | string; + /** Format: int32 */ + totalItemCount: number | string; + hasNextPage: boolean; + hasPreviousPage: boolean; + }; PaginatedResponseOfPlayListApplicationResponse: { items: components["schemas"]["PlayListApplicationResponse"][]; /** Format: int32 */ @@ -452,10 +590,10 @@ export interface components { /** Format: uuid */ id: string; name: string; - description: string; - smallImageKeyName: string; - mediumImageKeyName: string; - largeImageKeyName: string; + description: null | string; + smallImageKeyName: null | string; + mediumImageKeyName: null | string; + largeImageKeyName: null | string; /** Format: date-time */ createdAt: string; /** Format: date-time */ @@ -494,14 +632,16 @@ export interface components { /** Format: uuid */ id: string; title: string; - artist: string; + artist: null | string; source: components["schemas"]["TrackSource"]; - externalId: string; + externalId: null | string; audioStatus: components["schemas"]["ProcessingStatus"]; /** Format: int32 */ duration: number | string; /** Format: int32 */ listensCount: number | string; + /** Format: int64 */ + ownerUserId: null | number | string; /** Format: date-time */ createdAt: string; /** Format: date-time */ @@ -532,6 +672,12 @@ export interface components { /** Format: int32 */ expiresInSeconds: number | string; }; + UpdateAlbumRequest: { + newTitle: string; + newDescription: null | string; + /** Format: int32 */ + newReleaseYear: null | number | string; + }; UpdatePlayListRequest: { newName: null | string; newDescription: null | string; @@ -550,6 +696,10 @@ export interface components { /** Format: date-time */ updatedAt: string; }; + YouTubeArtistRef: { + id: null | string; + name: string; + }; YouTubeSearchResult: { items: components["schemas"]["YouTubeSongResult"][]; continuationToken: string; @@ -563,6 +713,7 @@ export interface components { durationSeconds: number | string; thumbnailUrl: string; isExplicit: boolean; + artists: components["schemas"]["YouTubeArtistRef"][]; }; /** @enum {unknown} */ YouTubeStreamMode: "Server" | "YouTube"; @@ -584,6 +735,330 @@ export interface components { } export type $defs = Record; export interface operations { + GetAlbums: { + parameters: { + query?: { + title?: string; + pageNumber?: number | string; + pageSize?: number | string; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseOfAlbumApplicationResponse"]; + }; + }; + }; + }; + CreateAlbum: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["CreateAlbumRequest"]; + }; + }; + responses: { + /** @description Created */ + 201: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlbumApplicationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GetAlbumById: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlbumApplicationResponse"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GetAlbumsByUserId: { + parameters: { + query?: { + pageNumber?: number | string; + pageSize?: number | string; + }; + header?: never; + path: { + userId: number | string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseOfAlbumApplicationResponse"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + GetAlbumTracks: { + parameters: { + query?: { + pageNumber?: number | string; + pageSize?: number | string; + }; + header?: never; + path: { + albumId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PaginatedResponseOfTrackApplicationResponse"]; + }; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + UpdateAlbum: { + parameters: { + query?: never; + header?: never; + path: { + albumId: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["UpdateAlbumRequest"]; + }; + }; + responses: { + /** @description OK */ + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["AlbumApplicationResponse"]; + }; + }; + /** @description Bad Request */ + 400: { + headers: { + [name: string]: unknown; + }; + content: { + "application/problem+json": components["schemas"]["HttpValidationProblemDetails"]; + }; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + DeleteAlbum: { + parameters: { + query?: never; + header?: never; + path: { + albumId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + AddTrackToAlbum: { + parameters: { + query?: never; + header?: never; + path: { + albumId: string; + trackId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @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; + }; + }; + }; + RemoveTrackFromAlbum: { + parameters: { + query?: never; + header?: never; + path: { + albumId: string; + trackId: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No Content */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Unauthorized */ + 401: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + /** @description Not Found */ + 404: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; GetPlayLists: { parameters: { query?: { diff --git a/web-player/src/lib/components/TrackTile.svelte b/web-player/src/lib/components/TrackTile.svelte index 62ddb70..cd6d9be 100644 --- a/web-player/src/lib/components/TrackTile.svelte +++ b/web-player/src/lib/components/TrackTile.svelte @@ -7,8 +7,8 @@ interface Props { id: string | number; title: string; - artist?: string; - coverSrc?: string; + artist?: string | null; + coverSrc?: string | null; coverSize?: 'small' | 'medium' | 'large'; hue: number; explicit?: boolean; diff --git a/web-player/src/lib/components/ui/Cover.svelte b/web-player/src/lib/components/ui/Cover.svelte index 8f799d6..7b0af9c 100644 --- a/web-player/src/lib/components/ui/Cover.svelte +++ b/web-player/src/lib/components/ui/Cover.svelte @@ -7,7 +7,7 @@ trackId: string | number; hue: number; size?: 'small' | 'medium' | 'large'; - src?: string; + src?: string | null; alt?: string; class?: string; children?: Snippet; diff --git a/web-player/src/lib/components/ui/Input.svelte b/web-player/src/lib/components/ui/Input.svelte index 75db04e..79be94e 100644 --- a/web-player/src/lib/components/ui/Input.svelte +++ b/web-player/src/lib/components/ui/Input.svelte @@ -5,7 +5,9 @@ value?: string; id?: string; name?: string; - type?: 'text' | 'search'; + type?: 'text' | 'search' | 'number'; + min?: number; + max?: number; placeholder?: string; maxlength?: number; required?: boolean; @@ -20,6 +22,8 @@ id, name, type = 'text', + min, + max, placeholder, maxlength, required = false, @@ -40,6 +44,8 @@ {id} {name} {type} + {min} + {max} {placeholder} {maxlength} {required} diff --git a/web-player/src/lib/components/ui/PlaylistCard.svelte b/web-player/src/lib/components/ui/PlaylistCard.svelte index 6143dc5..84b5c41 100644 --- a/web-player/src/lib/components/ui/PlaylistCard.svelte +++ b/web-player/src/lib/components/ui/PlaylistCard.svelte @@ -5,7 +5,7 @@ interface Props { id: string; name: string; - description?: string; + description?: string | null; trackIds: (string | number)[]; updatedAt: string; index?: number; diff --git a/web-player/src/lib/components/ui/TrackTitleCell.svelte b/web-player/src/lib/components/ui/TrackTitleCell.svelte index 4f28e64..20d502a 100644 --- a/web-player/src/lib/components/ui/TrackTitleCell.svelte +++ b/web-player/src/lib/components/ui/TrackTitleCell.svelte @@ -6,8 +6,8 @@ interface Props { trackId: string | number; title: string; - artist?: string; - coverSrc?: string; + artist?: string | null; + coverSrc?: string | null; coverSize?: string; hue: number; explicit?: boolean; diff --git a/web-player/src/lib/player/player.svelte.ts b/web-player/src/lib/player/player.svelte.ts index ac44251..a6e2fa9 100644 --- a/web-player/src/lib/player/player.svelte.ts +++ b/web-player/src/lib/player/player.svelte.ts @@ -34,9 +34,9 @@ export interface QueueItem { export interface ApiTrackLike { id: string | number; title: string; - artist?: string; + artist?: string | null; source?: 'Local' | 'YouTube'; - externalId?: string; + externalId?: string | null; audioStatus?: 'Pending' | 'Processing' | 'Completed' | 'Failed'; isExplicit?: boolean; } @@ -63,7 +63,7 @@ export function toQueueItems(tracks: ApiTrackLike[]): QueueItem[] { return { id: queueIdForTrack(track), title: track.title, - artist: track.artist, + artist: track.artist ?? undefined, source: youTube ? 'youtube' : 'local', coverUrl: youTube ? isPendingYouTubeTrack(track) && track.externalId diff --git a/web-player/src/lib/tracks.ts b/web-player/src/lib/tracks.ts index 6100ef1..9497f00 100644 --- a/web-player/src/lib/tracks.ts +++ b/web-player/src/lib/tracks.ts @@ -19,7 +19,7 @@ export function targetTitle(target: TrackTarget): string { } export function targetArtist(target: TrackTarget): string | undefined { - return target.kind === 'youtube' ? target.song.artist : target.track.artist; + return (target.kind === 'youtube' ? target.song.artist : target.track.artist) ?? undefined; } export function targetExplicit(target: TrackTarget): boolean { diff --git a/web-player/src/routes/(app)/playlists/[id]/+page.server.ts b/web-player/src/routes/(app)/playlists/[id]/+page.server.ts index 02b0d1a..f70d351 100644 --- a/web-player/src/routes/(app)/playlists/[id]/+page.server.ts +++ b/web-player/src/routes/(app)/playlists/[id]/+page.server.ts @@ -41,7 +41,7 @@ export const load: PageServerLoad = async ({ params, locals, url, fetch }) => { const alreadyLinked = new Set( tracks.filter((t) => t.source === 'YouTube' && t.externalId).map((t) => t.externalId) ); - const seeds = [...new Set(tracks.map((t) => t.artist).filter((a) => !!a))]; + const seeds = [...new Set(tracks.map((t) => t.artist).filter((a): a is string => !!a))]; const seed = seeds.length > 0 ? shuffle(seeds)[0] : undefined; const youtube = fetchYoutubeFiller( diff --git a/web-player/src/routes/(app)/playlists/[id]/+page.svelte b/web-player/src/routes/(app)/playlists/[id]/+page.svelte index 907f6f9..416fda9 100644 --- a/web-player/src/routes/(app)/playlists/[id]/+page.svelte +++ b/web-player/src/routes/(app)/playlists/[id]/+page.svelte @@ -66,7 +66,7 @@