Moved to ky for requests, significant improvements to YT client implementation with ky instances
This commit is contained in:
@@ -85,4 +85,7 @@
|
||||
#sidebar {
|
||||
grid-area: 2 / 1 / 3 / 2;
|
||||
}
|
||||
#content-wrapper {
|
||||
grid-area: 2 / 2 / 3 / 4;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
|
||||
@@ -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 }
|
||||
},
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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) =>
|
||||
|
||||
@@ -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-' })
|
||||
35
src/routes/api/v1/search/+server.ts
Normal file
35
src/routes/api/v1/search/+server.ts
Normal 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 })
|
||||
}
|
||||
Reference in New Issue
Block a user