diff --git a/package-lock.json b/package-lock.json index 909f1f1..2767cab 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,9 +19,7 @@ "musicbrainz-api": "^0.15.0", "node-vibrant": "^3.2.1-alpha.1", "pocketbase": "^0.21.1", - "type-fest": "^4.12.0", - "ytdl-core": "^4.11.5", - "zod": "^3.22.4" + "type-fest": "^4.12.0" }, "devDependencies": { "@sveltejs/adapter-auto": "^3.0.0", @@ -2885,18 +2883,6 @@ "node": "14 || >=16.14" } }, - "node_modules/m3u8stream": { - "version": "0.8.6", - "resolved": "https://registry.npmjs.org/m3u8stream/-/m3u8stream-0.8.6.tgz", - "integrity": "sha512-LZj8kIVf9KCphiHmH7sbFQTVe4tOemb202fWwvJwR9W5ENW/1hxJN6ksAWGhQgSBSa3jyWhnjKU1Fw1GaOdbyA==", - "dependencies": { - "miniget": "^4.2.2", - "sax": "^1.2.4" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/magic-string": { "version": "0.30.5", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.5.tgz", @@ -2995,14 +2981,6 @@ "node": ">=4" } }, - "node_modules/miniget": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/miniget/-/miniget-4.2.3.tgz", - "integrity": "sha512-SjbDPDICJ1zT+ZvQwK0hUcRY4wxlhhNpHL9nJOB2MEAXRGagTljsO8MEDzQMTFf0Q8g4QNi8P9lEm/g7e+qgzA==", - "engines": { - "node": ">=12" - } - }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", @@ -5130,27 +5108,6 @@ "engines": { "node": ">= 14" } - }, - "node_modules/ytdl-core": { - "version": "4.11.5", - "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz", - "integrity": "sha512-27LwsW4n4nyNviRCO1hmr8Wr5J1wLLMawHCQvH8Fk0hiRqrxuIu028WzbJetiYH28K8XDbeinYW4/wcHQD1EXA==", - "dependencies": { - "m3u8stream": "^0.8.6", - "miniget": "^4.2.2", - "sax": "^1.1.3" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/zod": { - "version": "3.22.4", - "resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz", - "integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } } } } diff --git a/package.json b/package.json index 6dfa97f..79144e7 100644 --- a/package.json +++ b/package.json @@ -38,8 +38,6 @@ "musicbrainz-api": "^0.15.0", "node-vibrant": "^3.2.1-alpha.1", "pocketbase": "^0.21.1", - "type-fest": "^4.12.0", - "ytdl-core": "^4.11.5", - "zod": "^3.22.4" + "type-fest": "^4.12.0" } } diff --git a/src/app.d.ts b/src/app.d.ts index 323f63d..4ced474 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -40,12 +40,52 @@ declare global { profilePicture?: string }) + type SearchFilterMap = + Filter extends 'song' ? Song : + Filter extends 'album' ? Album : + Filter extends 'artist' ? Artist : + Filter extends 'playlist' ? Playlist : + Filter extends undefined ? Song | Album | Artist | Playlist : + never + interface Connection { public id: string - getRecommendations: () => Promise<(Song | Album | Artist | Playlist)[]> getConnectionInfo: () => Promise - search: (searchTerm: string, filter?: 'song' | 'album' | 'artist' | 'playlist') => Promise<(Song | Album | Artist | Playlist)[]> - getAudioStream: (id: string, range: string | null) => Promise + getRecommendations: () => Promise<(Song | Album | Artist | Playlist)[]> + search: (searchTerm: string, filter?: T) => Promise[]> + + /** + * @param id The id of the requested song + * @param headers The request headers sent by the Lazuli client that need to be relayed to the connection's request to the server (e.g. 'range'). + * @returns A promise of response object containing the audio stream for the specified byte range + * + * Fetches the audio stream for a song. + */ + getAudioStream: (id: string, headers: Headers) => Promise + + /** + * @param id The id of an album + * @returns A promise of the album as an Album object + */ + getAlbum: (id: string) => Promise + + /** + * @param id The id of an album + * @returns A promise of the songs in the album as and array of Song objects + */ + getAlbumItems: (id: string) => Promise + + /** + * @param id The id of a playlist + * @returns A promise of the playlist of as a Playlist object + */ + getPlaylist: (id: string) => Promise + + /** + * @param id The id of a playlist + * @returns A promise of the songs in the playlist as and array of Song objects + */ + getPlaylistItems: (id: string) => Promise } // These Schemas should only contain general info data that is necessary for data fetching purposes. @@ -84,6 +124,7 @@ declare global { isVideo: boolean } + // Properties like duration and track count are properties of album items not the album itself type Album = { connection: { id: string @@ -92,15 +133,13 @@ declare global { id: string name: string type: 'album' - duration?: number // Seconds thumbnailUrl: string artists: { // Should try to order id: string name: string profilePicture?: string }[] | 'Various Artists' - releaseDate?: string // ISOString - length?: number + releaseYear?: string // #### } // Need to figure out how to do Artists, maybe just query MusicBrainz? @@ -129,8 +168,9 @@ declare global { name: string profilePicture?: string } - length: number } + + type HasDefinedProperty = T & { [P in K]-?: Exclude }; } export {} diff --git a/src/hooks.server.ts b/src/hooks.server.ts index b0c7c7d..1520da8 100644 --- a/src/hooks.server.ts +++ b/src/hooks.server.ts @@ -8,8 +8,23 @@ export const handle: Handle = async ({ event, resolve }) => { if (urlpath.startsWith('/api')) { const unprotectedAPIRoutes = ['/api/audio', '/api/remoteImage'] - const apikey = event.request.headers.get('apikey') || event.url.searchParams.get('apikey') - if (!unprotectedAPIRoutes.includes(urlpath) && apikey !== SECRET_INTERNAL_API_KEY) { + + function checkAuthorization(): boolean { + const apikey = event.request.headers.get('apikey') || event.url.searchParams.get('apikey') + if (apikey === SECRET_INTERNAL_API_KEY) return true + + const authToken = event.cookies.get('lazuli-auth') + if (!authToken) return false + + try { + jwt.verify(authToken, SECRET_JWT_KEY) + return true + } catch { + return false + } + } + + if (!unprotectedAPIRoutes.includes(urlpath) && !checkAuthorization()) { return new Response('Unauthorized', { status: 401 }) } } diff --git a/src/lib/components/media/mediaCard.svelte b/src/lib/components/media/mediaCard.svelte index 14e2ec1..0592bf7 100644 --- a/src/lib/components/media/mediaCard.svelte +++ b/src/lib/components/media/mediaCard.svelte @@ -8,10 +8,19 @@ let queueRef = $queue // This nonsense is to prevent an bug that causes svelte to throw an error when setting a property of the queue directly let image: HTMLImageElement, captionText: HTMLDivElement + + async function setQueueItems(mediaItem: Album | Playlist) { + const itemsResponse = await fetch(`/api/connections/${mediaItem.connection.id}/${mediaItem.type}/${mediaItem.id}/items`, { + credentials: 'include', + }).then((response) => response.json() as Promise<{ items: Song[] }>) + + const items = itemsResponse.items + queueRef.setQueue(...items) + }
-
{currentlyPlaying.name}
-
{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.uploader?.name}
+
{currentlyPlaying.artists?.map((artist) => artist.name).join(', ') ?? currentlyPlaying.uploader?.name}
@@ -101,8 +114,12 @@ -
- +
{:else} -
+
+
{#key currentlyPlaying} {/key}
-
-
Up next
- {#each $queue.list as item, index} +
+ UP NEXT + {#each $queue.list as item} {@const isCurrent = item === currentlyPlaying} {/each}
@@ -194,10 +213,42 @@
-
-
{currentlyPlaying.name}
-
- {currentlyPlaying.artists?.map((artist) => artist.name).join(', ') || currentlyPlaying.uploader?.name}{currentlyPlaying.album ? ` - ${currentlyPlaying.album.name}` : ''} +
+
+ (scrollDirection *= -1)} + class="{scrollDistance > 0 ? (scrollDirection > 0 ? 'scrollLeft' : 'scrollRight') : ''} scrollingText absolute whitespace-nowrap text-3xl">{currentlyPlaying.name} +
+
@@ -207,8 +258,12 @@ -
- +
{/if} -