Added library mixin to YTMusic and Jellyfin
This commit is contained in:
@@ -19,8 +19,6 @@
|
||||
playlistTooltip.style.left = `${x}px`
|
||||
playlistTooltip.style.top = `${y}px`
|
||||
}
|
||||
|
||||
let expanded = false
|
||||
</script>
|
||||
|
||||
<div class="h-full overflow-hidden">
|
||||
|
||||
@@ -1,13 +1,10 @@
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
||||
const getRecommendations = async (): Promise<(Song | Album | Artist | Playlist)[]> => {
|
||||
const recommendationResponse = await fetch(`/api/users/${locals.user.id}/recommendations`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())
|
||||
return recommendationResponse.recommendations
|
||||
}
|
||||
const getRecommendations = async () =>
|
||||
fetch(`/api/users/${locals.user.id}/recommendations`)
|
||||
.then((response) => response.json() as Promise<{ recommendations: (Song | Album | Artist | Playlist)[] }>)
|
||||
.then((data) => data.recommendations)
|
||||
|
||||
return { recommendations: getRecommendations() }
|
||||
}
|
||||
|
||||
@@ -1,23 +1,18 @@
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const connectionId = url.searchParams.get('connection')
|
||||
const id = url.searchParams.get('id')
|
||||
|
||||
async function getAlbum(): Promise<Album> {
|
||||
const albumResponse = (await fetch(`/api/connections/${connectionId}/album?id=${id}`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())) as { album: Album }
|
||||
return albumResponse.album
|
||||
}
|
||||
const getAlbum = async () =>
|
||||
fetch(`/api/connections/${connectionId}/album?id=${id}`)
|
||||
.then((response) => response.json() as Promise<{ album: Album }>)
|
||||
.then((data) => data.album)
|
||||
|
||||
async function getAlbumItems(): Promise<Song[]> {
|
||||
const itemsResponse = (await fetch(`/api/connections/${connectionId}/album/${id}/items`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())) as { items: Song[] }
|
||||
return itemsResponse.items
|
||||
}
|
||||
const getAlbumItems = async () =>
|
||||
fetch(`/api/connections/${connectionId}/album/${id}/items`)
|
||||
.then((response) => response.json() as Promise<{ items: Song[] }>)
|
||||
.then((data) => data.items)
|
||||
|
||||
return { albumDetails: Promise.all([getAlbum(), getAlbumItems()]) }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PageLoad } from './$types'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageLoad = async ({ fetch, url }) => {
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const connectionId = url.searchParams.get('connection')
|
||||
const id = url.searchParams.get('id')
|
||||
|
||||
22
src/routes/(app)/library/+page.server.ts
Normal file
22
src/routes/(app)/library/+page.server.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import type { PageServerLoad } from '../$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const connectionId = url.searchParams.get('connection')
|
||||
const id = url.searchParams.get('id')
|
||||
|
||||
async function getPlaylist() {
|
||||
const playlistResponse = (await fetch(`/api/connections/${connectionId}/playlist?id=${id}`, {
|
||||
credentials: 'include',
|
||||
}).then((response) => response.json())) as { playlist: Playlist }
|
||||
return playlistResponse.playlist
|
||||
}
|
||||
|
||||
async function getPlaylistItems() {
|
||||
const itemsResponse = (await fetch(`/api/connections/${connectionId}/playlist/${id}/items`, {
|
||||
credentials: 'include',
|
||||
}).then((response) => response.json())) as { items: Song[] }
|
||||
return itemsResponse.items
|
||||
}
|
||||
|
||||
return { playlistDetails: Promise.all([getPlaylist(), getPlaylistItems()]) }
|
||||
}
|
||||
@@ -1,16 +1,12 @@
|
||||
import type { PageServerLoad } from '../$types'
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url, locals }) => {
|
||||
const query = url.searchParams.get('query')
|
||||
if (query) {
|
||||
const getSearchResults = async (): Promise<(Song | Album | Artist | Playlist)[]> => {
|
||||
const searchResults = await fetch(`/api/search?query=${query}&userId=${locals.user.id}`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())
|
||||
return searchResults.searchResults
|
||||
}
|
||||
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)
|
||||
|
||||
return { searchResults: getSearchResults() }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { fail } from '@sveltejs/kit'
|
||||
import { SECRET_INTERNAL_API_KEY, YOUTUBE_API_CLIENT_SECRET } from '$env/static/private'
|
||||
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'
|
||||
@@ -7,13 +7,11 @@ import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
|
||||
import { google } from 'googleapis'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
const getConnectionInfo = async (): Promise<ConnectionInfo[]> => {
|
||||
const connectionInfoResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())
|
||||
return connectionInfoResponse.connections
|
||||
}
|
||||
const getConnectionInfo = async () =>
|
||||
fetch(`/api/users/${locals.user.id}/connections`)
|
||||
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
|
||||
.then((data) => data.connections)
|
||||
.catch(() => ({ error: 'Failed to retrieve connections' }))
|
||||
|
||||
return { connections: getConnectionInfo() }
|
||||
}
|
||||
@@ -31,19 +29,16 @@ export const actions: Actions = {
|
||||
|
||||
const newConnectionId = DB.addConnectionInfo({ userId: locals.user.id, type: 'jellyfin', service: { userId: authData.User.Id, serverUrl: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
|
||||
|
||||
const response = await fetch(`/api/connections?id=${newConnectionId}`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => {
|
||||
return response.json()
|
||||
})
|
||||
const newConnection = await fetch(`/api/connections?id=${newConnectionId}`)
|
||||
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
|
||||
.then((data) => data.connections[0])
|
||||
|
||||
return { newConnection: response.connections[0] }
|
||||
return { newConnection }
|
||||
},
|
||||
youtubeMusicLogin: async ({ request, fetch, locals }) => {
|
||||
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.
|
||||
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.
|
||||
const { tokens } = await client.getToken(code.toString())
|
||||
|
||||
const youtube = google.youtube('v3')
|
||||
@@ -57,14 +52,11 @@ export const actions: Actions = {
|
||||
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/connections?id=${newConnectionId}`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
})
|
||||
const newConnection = await fetch(`/api/connections?id=${newConnectionId}`)
|
||||
.then((response) => response.json() as Promise<{ connections: ConnectionInfo[] }>)
|
||||
.then((data) => data.connections[0])
|
||||
|
||||
const responseData = await response.json()
|
||||
|
||||
return { newConnection: responseData.connections[0] }
|
||||
return { newConnection }
|
||||
},
|
||||
deleteConnection: async ({ request }) => {
|
||||
const formData = await request.formData()
|
||||
|
||||
@@ -14,13 +14,13 @@
|
||||
import ConnectionProfile from './connectionProfile.svelte'
|
||||
import { enhance } from '$app/forms'
|
||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||
import { onMount } from 'svelte'
|
||||
import Loader from '$lib/components/util/loader.svelte'
|
||||
|
||||
export let data: PageServerData & LayoutData
|
||||
let connections: ConnectionInfo[]
|
||||
onMount(async () => {
|
||||
connections = await data.connections
|
||||
})
|
||||
let errorMessage: string
|
||||
|
||||
data.connections.then((userConnections) => ('error' in userConnections ? (errorMessage = userConnections.error) : (connections = userConnections)))
|
||||
|
||||
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
|
||||
const { serverUrl, username, password } = Object.fromEntries(formData)
|
||||
@@ -138,13 +138,21 @@
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
<div id="connection-profile-grid" class="grid gap-8">
|
||||
{#if connections}
|
||||
{#if connections}
|
||||
<div id="connection-profile-grid" class="grid gap-8">
|
||||
{#each connections as connectionInfo}
|
||||
<ConnectionProfile {connectionInfo} submitFunction={profileActions} />
|
||||
{/each}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if errorMessage}
|
||||
<div class="grid h-40 place-items-center">
|
||||
<span class="text-4xl">{errorMessage}</span>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="relative h-40">
|
||||
<Loader size={5} />
|
||||
</div>
|
||||
{/if}
|
||||
{#if newConnectionModal !== null}
|
||||
<svelte:component this={newConnectionModal} submitFunction={authenticateJellyfin} on:close={() => (newConnectionModal = null)} />
|
||||
{/if}
|
||||
|
||||
@@ -16,7 +16,7 @@ export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const limit = Number.isInteger(numberLimit) && numberLimit > 0 ? numberLimit : undefined
|
||||
|
||||
const response = await connection
|
||||
.getPlaylistItems(playlistId!, startIndex, limit)
|
||||
.getPlaylistItems(playlistId!, { startIndex, limit })
|
||||
.then((items) => Response.json({ items }))
|
||||
.catch((error: TypeError | Error) => {
|
||||
if (error instanceof TypeError) return new Response('Bad Request', { status: 400 })
|
||||
|
||||
13
src/routes/api/users/[userId]/library/albums/+server.ts
Normal file
13
src/routes/api/users/[userId]/library/albums/+server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
const items = (await Promise.all(userConnections.map((connection) => connection.library.albums()))).flat()
|
||||
|
||||
return Response.json({ items })
|
||||
}
|
||||
13
src/routes/api/users/[userId]/library/artists/+server.ts
Normal file
13
src/routes/api/users/[userId]/library/artists/+server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
const items = (await Promise.all(userConnections.map((connection) => connection.library.artists()))).flat()
|
||||
|
||||
return Response.json({ items })
|
||||
}
|
||||
13
src/routes/api/users/[userId]/library/playlists/+server.ts
Normal file
13
src/routes/api/users/[userId]/library/playlists/+server.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
const items = (await Promise.all(userConnections.map((connection) => connection.library.playlists()))).flat()
|
||||
|
||||
return Response.json({ items })
|
||||
}
|
||||
Reference in New Issue
Block a user