From 11497f8b9108ccc9d34ad0227c8434261fe7d812 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Tue, 28 May 2024 00:46:34 -0400 Subject: [PATCH] Dropped ytdl, YT audio now fetched with Android Client. Began work on YT Premium support --- package-lock.json | 45 +- package.json | 4 +- src/app.d.ts | 54 +- src/hooks.server.ts | 19 +- src/lib/components/media/mediaCard.svelte | 26 +- src/lib/components/media/mediaPlayer.svelte | 169 +++++-- src/lib/components/util/loader.svelte | 5 +- src/lib/components/util/slider.svelte | 6 +- src/lib/server/connections.ts | 21 +- src/lib/server/db.ts | 27 +- src/lib/server/youtube-music-types.d.ts | 214 +++++--- src/lib/server/youtube-music.ts | 466 +++++++++++++----- src/lib/stores.ts | 6 + .../(app)/details/album/+page.server.ts | 23 + src/routes/(app)/details/album/+page.svelte | 34 ++ src/routes/(app)/search/+page.svelte | 7 +- src/routes/(app)/user/+page.server.ts | 4 +- src/routes/api/audio/+server.ts | 32 +- src/routes/api/connections/+server.ts | 23 +- .../[connectionId]/album/+server.ts | 16 + .../album/[albumId]/items/+server.ts | 13 + .../[connectionId]/artist/+server.ts | 0 .../[connectionId]/playlist/+server.ts | 16 + .../playlist/[playlistId]/items/+server.ts | 13 + src/routes/api/remoteImage/+server.ts | 9 +- src/routes/api/search/+server.ts | 31 +- .../api/users/[userId]/connections/+server.ts | 20 +- .../users/[userId]/recommendations/+server.ts | 22 +- 28 files changed, 932 insertions(+), 393 deletions(-) create mode 100644 src/routes/(app)/details/album/+page.server.ts create mode 100644 src/routes/(app)/details/album/+page.svelte create mode 100644 src/routes/api/connections/[connectionId]/album/+server.ts create mode 100644 src/routes/api/connections/[connectionId]/album/[albumId]/items/+server.ts create mode 100644 src/routes/api/connections/[connectionId]/artist/+server.ts create mode 100644 src/routes/api/connections/[connectionId]/playlist/+server.ts create mode 100644 src/routes/api/connections/[connectionId]/playlist/[playlistId]/items/+server.ts 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} -