Moved to ky for requests, significant improvements to YT client implementation with ky instances

This commit is contained in:
Eclypsed
2024-07-04 02:54:24 -04:00
parent f17773838a
commit 8453e51d3f
33 changed files with 2245 additions and 1370 deletions

View File

@@ -85,4 +85,7 @@
#sidebar {
grid-area: 2 / 1 / 3 / 2;
}
#content-wrapper {
grid-area: 2 / 2 / 3 / 4;
}
</style>

View File

@@ -2,9 +2,9 @@
import LazyImage from '$lib/components/media/lazyImage.svelte'
import IconButton from '$lib/components/util/iconButton.svelte'
import ArtistList from '$lib/components/media/artistList.svelte'
import Services from '$lib/services.json'
import { goto } from '$app/navigation'
import { queue, newestAlert } from '$lib/stores'
import ServiceLogo from '$lib/components/util/serviceLogo.svelte'
export let album: Album
@@ -33,7 +33,9 @@
<i slot="icon" class="fa-solid fa-play text-2xl" />
</IconButton>
</div>
<img id="connection-type-icon" class="absolute left-2 top-2 h-9 w-9 opacity-0 transition-opacity" src={Services[album.connection.type].icon} alt={Services[album.connection.type].displayName} />
<div id="connection-type-icon" class="absolute left-2 top-2 h-9 w-9 opacity-0 transition-opacity">
<ServiceLogo type={album.connection.type} />
</div>
</div>
<div class="py-2 text-center text-sm">
<div class="line-clamp-2">{album.name}</div>

View File

@@ -4,9 +4,9 @@ export const load: PageServerLoad = async ({ fetch, url, locals }) => {
const query = url.searchParams.get('query')
if (query) {
const getSearchResults = async () =>
fetch(`/api/search?query=${query}&userId=${locals.user.id}`, {})
.then((response) => response.json() as Promise<{ searchResults: (Song | Album | Artist | Playlist)[] }>)
.then((data) => data.searchResults)
fetch(`/api/v1/search?query=${query}&userId=${locals.user.id}&types=song,album,artist,playlist`)
.then((response) => response.json() as Promise<{ results: (Song | Album | Artist | Playlist)[] }>)
.then((data) => data.results)
return { searchResults: getSearchResults() }
}

View File

@@ -3,29 +3,31 @@ import { YOUTUBE_API_CLIENT_SECRET } from '$env/static/private'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
import type { PageServerLoad, Actions } from './$types'
import { DB } from '$lib/server/db'
import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
import { Jellyfin } from '$lib/server/jellyfin'
import { google } from 'googleapis'
import ky from 'ky'
export const load: PageServerLoad = async ({ fetch, locals }) => {
const getConnectionInfo = async () =>
fetch(`/api/users/${locals.user.id}/connections`)
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
.then((data) => data.connections)
export const load: PageServerLoad = async ({ fetch, locals, url }) => {
const getConnectionInfo = () =>
ky
.get(`api/users/${locals.user.id}/connections`, { fetch, prefixUrl: url.origin })
.json<{ connections: ConnectionInfo[] }>()
.then((response) => response.connections)
.catch(() => ({ error: 'Failed to retrieve connections' }))
return { connections: getConnectionInfo() }
}
export const actions: Actions = {
authenticateJellyfin: async ({ request, fetch, locals }) => {
authenticateJellyfin: async ({ request, fetch, locals, url }) => {
const formData = await request.formData()
const { serverUrl, username, password, deviceId } = Object.fromEntries(formData)
if (!URL.canParse(serverUrl.toString())) return fail(400, { message: 'Invalid Server URL' })
const authData = await Jellyfin.authenticateByName(username.toString(), password.toString(), new URL(serverUrl.toString()), deviceId.toString()).catch((error: JellyfinFetchError) => error)
const authData = await Jellyfin.authenticateByName(username.toString(), password.toString(), new URL(serverUrl.toString()), deviceId.toString()).catch(() => null)
if (authData instanceof JellyfinFetchError) return fail(authData.httpCode, { message: authData.message })
if (!authData) return fail(400, { message: 'Failed to Authenticate' })
const userId = locals.user.id
const serviceUserId = authData.User.Id
@@ -33,13 +35,12 @@ export const actions: Actions = {
const newConnectionId = await DB.connections.insert({ id: DB.uuid(), userId, type: 'jellyfin', serviceUserId, serverUrl: serverUrl.toString(), accessToken }, 'id').then((data) => data[0].id)
const newConnection = await fetch(`/api/connections?id=${newConnectionId}`)
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
.then((data) => data.connections[0])
const connectionsResponse = await ky.get(`api/connections?id=${newConnectionId}`, { fetch, prefixUrl: url.origin }).json<{ connections: ConnectionInfo[] }>()
const newConnection = connectionsResponse.connections[0]
return { newConnection }
},
youtubeMusicLogin: async ({ request, fetch, locals }) => {
youtubeMusicLogin: async ({ request, fetch, locals, url }) => {
const formData = await request.formData()
const { code } = Object.fromEntries(formData)
const client = new google.auth.OAuth2({ clientId: PUBLIC_YOUTUBE_API_CLIENT_ID, clientSecret: YOUTUBE_API_CLIENT_SECRET, redirectUri: 'http://localhost:5173' }) // ! DO NOT SHIP THIS. THE CLIENT SECRET SHOULD NOT BE MADE AVAILABLE TO USERS. MAKE A REQUEST TO THE LAZULI WEBSITE INSTEAD.
@@ -56,9 +57,8 @@ export const actions: Actions = {
.insert({ id: DB.uuid(), userId, type: 'youtube-music', serviceUserId, accessToken: access_token!, refreshToken: refresh_token!, expiry: expiry_date! }, 'id')
.then((data) => data[0].id)
const newConnection = await fetch(`/api/connections?id=${newConnectionId}`)
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
.then((data) => data.connections[0])
const connectionsResponse = await ky.get(`api/connections?id=${newConnectionId}`, { fetch, prefixUrl: url.origin }).json<{ connections: ConnectionInfo[] }>()
const newConnection = connectionsResponse.connections[0]
return { newConnection }
},

View File

@@ -3,8 +3,6 @@
import { goto } from '$app/navigation'
import type { LayoutData } from '../$types'
import Services from '$lib/services.json'
import JellyfinIcon from '$lib/static/jellyfin-icon.svg'
import YouTubeMusicIcon from '$lib/static/youtube-music-icon.svg'
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
import { newestAlert } from '$lib/stores.js'
import type { PageServerData } from './$types.js'
@@ -15,6 +13,7 @@
import { enhance } from '$app/forms'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
import Loader from '$lib/components/util/loader.svelte'
import ServiceLogo from '$lib/components/util/serviceLogo.svelte'
export let data: PageServerData & LayoutData
let connections: ConnectionInfo[]
@@ -129,11 +128,15 @@
<h1 class="py-2 text-xl">Add Connection</h1>
<div class="flex flex-wrap gap-2 pb-4">
<button class="add-connection-button h-14 rounded-md" on:click={() => (newConnectionModal = JellyfinAuthBox)}>
<img src={JellyfinIcon} alt="Jellyfin icon" class="aspect-square h-full p-2" />
<div class="aspect-square h-full p-2">
<ServiceLogo type={'jellyfin'} />
</div>
</button>
<form method="post" action="?/youtubeMusicLogin" use:enhance={authenticateYouTube}>
<button class="add-connection-button h-14 rounded-md">
<img src={YouTubeMusicIcon} alt="YouTube Music icon" class="aspect-square h-full p-2" />
<div class="aspect-square h-full p-2">
<ServiceLogo type={'youtube-music'} />
</div>
</button>
</form>
</div>

View File

@@ -1,11 +1,11 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ url }) => {
const ids = url.searchParams.get('id')?.replace(/\s/g, '').split(',')
if (!ids) return new Response('Missing id query parameter', { status: 400 })
const connections = (await Promise.all(ids.map((id) => buildConnection(id).catch(() => null)))).filter((result): result is Connection => result !== null)
const connections = (await Promise.all(ids.map((id) => ConnectionFactory.getConnection(id).catch(() => null)))).filter((result): result is Connection => result !== null)
const getConnectionInfo = (connection: Connection) =>
connection.getConnectionInfo().catch((reason) => {

View File

@@ -1,9 +1,9 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params, url }) => {
const connectionId = params.connectionId!
const connection = await buildConnection(connectionId).catch(() => null)
const connection = await ConnectionFactory.getConnection(connectionId).catch(() => null)
if (!connection) return new Response('Invalid connection id', { status: 400 })
const albumId = url.searchParams.get('id')

View File

@@ -1,10 +1,10 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params }) => {
const { connectionId, albumId } = params
const connection = await buildConnection(connectionId!).catch(() => null)
const connection = await ConnectionFactory.getConnection(connectionId!).catch(() => null)
if (!connection) return new Response('Invalid connection id', { status: 400 })
const items = await connection.getAlbumItems(albumId!).catch(() => null)

View File

@@ -1,9 +1,9 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params, url }) => {
const connectionId = params.connectionId!
const connection = await buildConnection(connectionId).catch(() => null)
const connection = await ConnectionFactory.getConnection(connectionId).catch(() => null)
if (!connection) return new Response('Invalid connection id', { status: 400 })
const playlistId = url.searchParams.get('id')

View File

@@ -1,9 +1,9 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params, url }) => {
const { connectionId, playlistId } = params
const connection = await buildConnection(connectionId!).catch(() => null)
const connection = await ConnectionFactory.getConnection(connectionId!).catch(() => null)
if (!connection) return new Response('Invalid connection id', { status: 400 })
const startIndexString = url.searchParams.get('startIndex')

View File

@@ -1,23 +0,0 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ url }) => {
const { query, userId, filter } = Object.fromEntries(url.searchParams) as { [k: string]: string | undefined }
if (!(query && userId)) return new Response('Missing search parameter', { status: 400 })
const userConnections = await buildUserConnections(userId).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
let checkedFilter: 'song' | 'album' | 'artist' | 'playlist' | undefined
if (filter === 'song' || filter === 'album' || filter === 'artist' || filter === 'playlist') checkedFilter = filter
const search = (connection: Connection) =>
connection.search(query, checkedFilter).catch((reason) => {
console.error(`Failed to search "${query}" from connection ${connection.id}: ${reason}`)
return null
})
const searchResults = (await Promise.all(userConnections.map(search))).flat().filter((result): result is Song | Album | Artist | Playlist => result !== null)
return Response.json({ searchResults })
}

View File

@@ -1,8 +1,8 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params }) => {
const userConnections = await buildUserConnections(params.userId!).catch(() => null)
const userConnections = await ConnectionFactory.getUserConnections(params.userId!).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
const getConnectionInfo = (connection: Connection) =>

View File

@@ -1,8 +1,8 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params }) => {
const userConnections = await buildUserConnections(params.userId!).catch(() => null)
const userConnections = await ConnectionFactory.getUserConnections(params.userId!).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
const items = (await Promise.all(userConnections.map((connection) => connection.library.albums()))).flat()

View File

@@ -1,8 +1,8 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params }) => {
const userConnections = await buildUserConnections(params.userId!).catch(() => null)
const userConnections = await ConnectionFactory.getUserConnections(params.userId!).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
const items = (await Promise.all(userConnections.map((connection) => connection.library.artists()))).flat()

View File

@@ -1,8 +1,8 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ params }) => {
const userConnections = await buildUserConnections(params.userId!).catch(() => null)
const userConnections = await ConnectionFactory.getUserConnections(params.userId!).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
const items = (await Promise.all(userConnections.map((connection) => connection.library.playlists()))).flat()

View File

@@ -1,10 +1,10 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildUserConnections } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
// This is temporary functionally for the sake of developing the app.
// In the future will implement more robust algorithm for offering recommendations
export const GET: RequestHandler = async ({ params }) => {
const userConnections = await buildUserConnections(params.userId!).catch(() => null)
const userConnections = await ConnectionFactory.getUserConnections(params.userId!).catch(() => null)
if (!userConnections) return new Response('Invalid user id', { status: 400 })
const getRecommendations = (connection: Connection) =>

View File

@@ -1,12 +1,12 @@
import type { RequestHandler } from '@sveltejs/kit'
import { buildConnection } from '$lib/server/api-helper'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ url, request }) => {
const connectionId = url.searchParams.get('connection')
const id = url.searchParams.get('id')
if (!(connectionId && id)) return new Response('Missing query parameter', { status: 400 })
// Might want to re-evaluate how specific I make these ^ v error response messages
const connection = await buildConnection(connectionId).catch(() => null)
const connection = await ConnectionFactory.getConnection(connectionId).catch(() => null)
if (!connection) return new Response('Invalid connection id', { status: 400 })
const audioRequestHeaders = new Headers({ range: request.headers.get('range') ?? 'bytes=0-' })

View File

@@ -0,0 +1,35 @@
import type { RequestHandler } from '@sveltejs/kit'
import { ConnectionFactory } from '$lib/server/api-helper'
export const GET: RequestHandler = async ({ url }) => {
const query = url.searchParams.get('query')
const userId = url.searchParams.get('userId')
const typeSet = new Set<'song' | 'album' | 'artist' | 'playlist'>()
url.searchParams
.get('types')
?.toLowerCase()
.split(',')
.forEach((type) => {
type = type.trim()
if (type === 'song' || type === 'album' || type === 'artist' || type === 'playlist') {
typeSet.add(type)
}
})
if (!(query && userId && typeSet.size > 0)) return new Response('Bad Request', { status: 400 })
const userConnections = await ConnectionFactory.getUserConnections(userId).catch(() => null)
if (!userConnections) return new Response('Bad Request', { status: 400 })
const search = (connection: Connection) =>
connection.search(query, typeSet).catch((reason) => {
console.error(`Failed to search "${query}" from connection ${connection.id}: ${reason}`)
return null
})
const results = await Promise.all(userConnections.map(search)).then((results) => results.flat().filter((result) => result !== null))
return Response.json({ results })
}