Created ConnectionProfile class to encapsulate connection data for display on connection page
This commit is contained in:
@@ -26,12 +26,8 @@ export async function GET({ url, fetch }) {
|
||||
return authResponse
|
||||
}
|
||||
|
||||
if (!authResponse.headers.get('content-type').includes('application/json')) return new Response('Jellyfin server returned invalid data', { status: 500 })
|
||||
|
||||
const data = await authResponse.json()
|
||||
const requiredData = ['User', 'AccessToken', 'ServerId']
|
||||
|
||||
if (!requiredData.every((key) => Object.keys(data).includes(key))) return new Response('Data missing from Jellyfin server response', { status: 500 })
|
||||
if (!('AccessToken' in data && 'User' in data)) return new Response('Jellyfin server response has missing data', { status: 500 })
|
||||
|
||||
const responseData = JSON.stringify(data)
|
||||
const responseHeaders = new Headers({
|
||||
|
||||
@@ -2,70 +2,67 @@ import { UserConnections } from '$lib/server/db/users'
|
||||
import Joi from 'joi'
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function GET({ request, url }) {
|
||||
const schema = Joi.number().required()
|
||||
const userId = request.headers.get('userId')
|
||||
|
||||
const validation = schema.validate(userId)
|
||||
if (validation.error) return new Response(validation.error.message, { status: 400 })
|
||||
export async function GET({ url }) {
|
||||
const { userId, filter } = Object.fromEntries(url.searchParams)
|
||||
if (!userId) return new Response('Requires User Id', { status: 400 })
|
||||
|
||||
const responseHeaders = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
})
|
||||
|
||||
const filter = url.searchParams.get('filter')
|
||||
if (filter) {
|
||||
const requestedConnections = filter.split(',').map((item) => item.toLowerCase())
|
||||
const userConnections = UserConnections.getConnections(userId, requestedConnections)
|
||||
const userConnections = UserConnections.getUserConnections(userId, requestedConnections)
|
||||
return new Response(JSON.stringify(userConnections), { headers: responseHeaders })
|
||||
}
|
||||
|
||||
const userConnections = UserConnections.getConnections(userId)
|
||||
const userConnections = UserConnections.getUserConnections(userId)
|
||||
return new Response(JSON.stringify(userConnections), { headers: responseHeaders })
|
||||
}
|
||||
|
||||
// May need to add support for refresh token and expiry in the future
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function PATCH({ request }) {
|
||||
export async function PATCH({ request, url }) {
|
||||
const schema = Joi.object({
|
||||
userId: Joi.number().required(),
|
||||
userId: Joi.required(),
|
||||
connection: Joi.object({
|
||||
serviceType: Joi.string().required(),
|
||||
serviceUserId: Joi.string().required(),
|
||||
serviceUrl: Joi.string().required(),
|
||||
accessToken: Joi.string().required(),
|
||||
refreshToken: Joi.string(),
|
||||
expiry: Joi.number(),
|
||||
connectionInfo: Joi.string(),
|
||||
}).required(),
|
||||
})
|
||||
|
||||
const userId = request.headers.get('userId')
|
||||
const userId = url.searchParams.get('userId')
|
||||
const connection = await request.json()
|
||||
|
||||
const validation = schema.validate({ userId, connection })
|
||||
if (validation.error) return new Response(validation.error.message, { status: 400 })
|
||||
|
||||
const { serviceType, accessToken, refreshToken, expiry, connectionInfo } = connection
|
||||
const newConnectionId = UserConnections.addConnection(userId, serviceType, accessToken, { refreshToken, expiry, connectionInfo })
|
||||
const { serviceType, serviceUserId, serviceUrl, accessToken, refreshToken, expiry } = connection
|
||||
const newConnectionId = UserConnections.addConnection(userId, serviceType, serviceUserId, serviceUrl, accessToken, { refreshToken, expiry })
|
||||
|
||||
return new Response(JSON.stringify({ id: newConnectionId }))
|
||||
}
|
||||
|
||||
/** @type {import('./$types').RequestHandler} */
|
||||
export async function DELETE({ request }) {
|
||||
export async function DELETE({ request, url }) {
|
||||
const schema = Joi.object({
|
||||
userId: Joi.number().required(),
|
||||
userId: Joi.required(),
|
||||
connection: Joi.object({
|
||||
serviceId: Joi.string().required(),
|
||||
connectionId: Joi.string().required(),
|
||||
}).required(),
|
||||
})
|
||||
|
||||
const userId = request.headers.get('userId')
|
||||
const userId = url.searchParams.get('userId')
|
||||
const connection = await request.json()
|
||||
|
||||
const validation = schema.validate({ userId, connection })
|
||||
if (validation.error) return new Response(validation.error.message, { status: 400 })
|
||||
|
||||
const deletedConnectionId = UserConnections.deleteConnection(userId, connection.serviceId)
|
||||
UserConnections.deleteConnection(userId, connection.connectionId)
|
||||
|
||||
return new Response(JSON.stringify({ id: deletedConnectionId }))
|
||||
return new Response('Connection deleted', { status: 200 })
|
||||
}
|
||||
|
||||
@@ -1,29 +1,62 @@
|
||||
import { fail } from '@sveltejs/kit'
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import { UserConnections } from '$lib/server/db/users'
|
||||
|
||||
class ConnectionProfile {
|
||||
static async createProfile(connectionId) {
|
||||
const connectionData = await this.getUserData(connectionId)
|
||||
return { connectionId, ...connectionData }
|
||||
}
|
||||
|
||||
static getUserData = async (connectionId) => {
|
||||
const connectionData = UserConnections.getConnection(connectionId)
|
||||
const { serviceType, serviceUserId, serviceUrl, accessToken } = connectionData
|
||||
|
||||
switch (serviceType) {
|
||||
case 'jellyfin':
|
||||
const userUrl = new URL(`Users/${serviceUserId}`, serviceUrl).href
|
||||
const systemUrl = new URL('System/Info', serviceUrl).href
|
||||
|
||||
const reqHeaders = new Headers()
|
||||
reqHeaders.append('Authorization', `MediaBrowser Token="${accessToken}"`)
|
||||
|
||||
const userResponse = await fetch(userUrl, { headers: reqHeaders })
|
||||
const systemResponse = await fetch(systemUrl, { headers: reqHeaders })
|
||||
|
||||
const userData = await userResponse.json()
|
||||
const systemData = await systemResponse.json()
|
||||
|
||||
return {
|
||||
serviceType,
|
||||
userId: serviceUserId,
|
||||
username: userData?.Name,
|
||||
serviceUrl: serviceUrl,
|
||||
serverName: systemData?.ServerName,
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export const load = async ({ fetch, locals }) => {
|
||||
const response = await fetch('/api/user/connections', {
|
||||
const response = await fetch(`/api/user/connections?userId=${locals.userId}`, {
|
||||
headers: {
|
||||
apikey: SECRET_INTERNAL_API_KEY,
|
||||
userId: locals.userId,
|
||||
},
|
||||
})
|
||||
if (response.ok) {
|
||||
const connectionData = await response.json()
|
||||
const clientConnectionData = {}
|
||||
connectionData?.forEach((connection) => {
|
||||
const { id, serviceType, connectionInfo } = connection
|
||||
clientConnectionData[id] = {
|
||||
serviceType,
|
||||
connectionInfo: JSON.parse(connectionInfo),
|
||||
}
|
||||
})
|
||||
return { existingConnections: clientConnectionData }
|
||||
} else {
|
||||
const error = await response.text()
|
||||
console.log(error)
|
||||
|
||||
const allConnections = await response.json()
|
||||
const connectionProfiles = []
|
||||
if (allConnections) {
|
||||
for (const connection of allConnections) {
|
||||
const connectionProfile = await ConnectionProfile.createProfile(connection.id)
|
||||
connectionProfiles.push(connectionProfile)
|
||||
}
|
||||
}
|
||||
|
||||
return { connectionProfiles }
|
||||
}
|
||||
|
||||
/** @type {import('./$types').Actions}} */
|
||||
@@ -35,6 +68,7 @@ export const actions = {
|
||||
const [key, value] = field
|
||||
queryParams.append(key, value)
|
||||
}
|
||||
|
||||
const jellyfinAuthResponse = await fetch(`/api/jellyfin/auth?${queryParams.toString()}`, {
|
||||
headers: {
|
||||
apikey: SECRET_INTERNAL_API_KEY,
|
||||
@@ -47,40 +81,38 @@ export const actions = {
|
||||
}
|
||||
|
||||
const jellyfinAuthData = await jellyfinAuthResponse.json()
|
||||
const { User, AccessToken, ServerId } = jellyfinAuthData
|
||||
const connectionInfo = JSON.stringify({ User, ServerId })
|
||||
const updateConnectionsResponse = await fetch('/api/user/connections', {
|
||||
const accessToken = jellyfinAuthData.AccessToken
|
||||
const jellyfinUserId = jellyfinAuthData.User.Id
|
||||
const updateConnectionsResponse = await fetch(`/api/user/connections?userId=${locals.userId}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
apikey: SECRET_INTERNAL_API_KEY,
|
||||
userId: locals.userId,
|
||||
},
|
||||
body: JSON.stringify({ serviceType: 'jellyfin', accessToken: AccessToken, connectionInfo }),
|
||||
body: JSON.stringify({ serviceType: 'jellyfin', serviceUserId: jellyfinUserId, serviceUrl: formData.get('serverUrl'), accessToken }),
|
||||
})
|
||||
|
||||
if (!updateConnectionsResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
||||
|
||||
const newConnectionData = await updateConnectionsResponse.json()
|
||||
|
||||
return { message: 'Added Jellyfin connection', newConnection: { id: newConnectionData.id, serviceType: 'jellyfin', connectionInfo: JSON.parse(connectionInfo) } }
|
||||
const jellyfinProfile = await ConnectionProfile.createProfile(newConnectionData.id)
|
||||
|
||||
return { newConnection: jellyfinProfile }
|
||||
},
|
||||
deleteConnection: async ({ request, fetch, locals }) => {
|
||||
const formData = await request.formData()
|
||||
const serviceId = formData.get('serviceId')
|
||||
const connectionId = formData.get('connectionId')
|
||||
|
||||
const deleteConnectionResponse = await fetch('/api/user/connections', {
|
||||
const deleteConnectionResponse = await fetch(`/api/user/connections?userId=${locals.userId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
apikey: SECRET_INTERNAL_API_KEY,
|
||||
userId: locals.userId,
|
||||
},
|
||||
body: JSON.stringify({ serviceId }),
|
||||
body: JSON.stringify({ connectionId }),
|
||||
})
|
||||
|
||||
if (!deleteConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
|
||||
|
||||
const deletedConnectionData = await deleteConnectionResponse.json()
|
||||
|
||||
return { message: 'Connection deleted', deletedConnection: { id: deletedConnectionData.id } }
|
||||
return { deletedConnectionId: connectionId }
|
||||
},
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
import Toggle from '$lib/components/utility/toggle.svelte'
|
||||
|
||||
export let data
|
||||
let existingConnections = data.existingConnections
|
||||
let connectionProfiles = data.connectionProfiles
|
||||
|
||||
const submitCredentials = ({ formData, action, cancel }) => {
|
||||
switch (action.search) {
|
||||
@@ -43,18 +43,20 @@
|
||||
case 'success':
|
||||
modal = null
|
||||
if (result.data?.newConnection) {
|
||||
const { id, serviceType, connectionInfo } = result.data.newConnection
|
||||
existingConnections[id] = {
|
||||
serviceType,
|
||||
connectionInfo,
|
||||
}
|
||||
existingConnections = existingConnections
|
||||
} else if (result.data?.deletedConnection) {
|
||||
const id = result.data.deletedConnection.id
|
||||
delete existingConnections[id]
|
||||
existingConnections = existingConnections
|
||||
const newConnection = result.data.newConnection
|
||||
connectionProfiles = [newConnection, ...connectionProfiles]
|
||||
|
||||
return ($newestAlert = ['success', `Added ${Services[newConnection.serviceType].displayName}`])
|
||||
} else if (result.data?.deletedConnectionId) {
|
||||
const id = result.data.deletedConnectionId
|
||||
const indexToDelete = connectionProfiles.findIndex((profile) => profile.connectionId === id)
|
||||
const serviceType = connectionProfiles[indexToDelete].serviceType
|
||||
|
||||
connectionProfiles.splice(indexToDelete, 1)
|
||||
connectionProfiles = connectionProfiles
|
||||
|
||||
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
|
||||
}
|
||||
return ($newestAlert = ['success', result.data.message])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,21 +82,22 @@
|
||||
</div>
|
||||
</section>
|
||||
<div class="grid gap-8">
|
||||
{#each Object.entries(existingConnections) as [connectionId, connectionData]}
|
||||
{@const serviceData = Services[connectionData.serviceType]}
|
||||
{#each connectionProfiles as connectionProfile}
|
||||
{@const serviceData = Services[connectionProfile.serviceType]}
|
||||
<section class="overflow-hidden rounded-lg" style="background-color: rgba(82, 82, 82, 0.25);" transition:fly={{ x: 50 }}>
|
||||
<header class="flex h-20 items-center gap-4 p-4">
|
||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" class="aspect-square h-full p-1" />
|
||||
<div>
|
||||
{#if connectionData.serviceType === 'jellyfin'}
|
||||
<div>{connectionData.connectionInfo.User.Name}</div>
|
||||
{:else}
|
||||
<div>Account Name</div>
|
||||
{/if}
|
||||
<div class="text-sm text-neutral-500">{serviceData.displayName}</div>
|
||||
<div>{connectionProfile?.username ? connectionProfile.username : 'Placeholder Account Name'}</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
{serviceData.displayName}
|
||||
{#if connectionProfile.serviceType === 'jellyfin'}
|
||||
- {connectionProfile.serverName}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="ml-auto h-8">
|
||||
<IconButton on:click={() => (modal = `delete-${connectionId}`)}>
|
||||
<IconButton on:click={() => (modal = `delete-${connectionProfile.connectionId}`)}>
|
||||
<i slot="icon" class="fa-solid fa-link-slash" />
|
||||
</IconButton>
|
||||
</div>
|
||||
@@ -113,11 +116,12 @@
|
||||
<form method="post" use:enhance={submitCredentials} transition:fly={{ y: -15 }} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2">
|
||||
{#if typeof modal === 'string'}
|
||||
{@const connectionId = modal.replace('delete-', '')}
|
||||
{@const service = Services[existingConnections[connectionId].serviceType]}
|
||||
{@const connection = connectionProfiles.find((profile) => profile.connectionId === connectionId)}
|
||||
{@const serviceData = Services[connection.serviceType]}
|
||||
<div class="rounded-lg bg-neutral-900 p-5">
|
||||
<h1 class="pb-4 text-center">Delete {service.displayName} connection?</h1>
|
||||
<h1 class="pb-4 text-center">Delete {serviceData.displayName} connection?</h1>
|
||||
<div class="flex w-60 justify-around">
|
||||
<input type="hidden" name="serviceId" value={connectionId} />
|
||||
<input type="hidden" name="connectionId" value={connectionId} />
|
||||
<button class="rounded bg-neutral-800 px-4 py-2 text-center" on:click|preventDefault={() => (modal = null)}>Cancel</button>
|
||||
<button class="rounded bg-red-500 px-4 py-2 text-center" formaction="?/deleteConnection">Delete</button>
|
||||
</div>
|
||||
|
||||
12
src/routes/settings/connections/userDataSchemaRef.json
Normal file
12
src/routes/settings/connections/userDataSchemaRef.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"connectionId": "Database id of the respective connection",
|
||||
"serviceType": "Type of service of the respective connection",
|
||||
"userId": "The UUID of the account [req]",
|
||||
"username": "The username of the account [req]",
|
||||
"email": "Email asscociated w/ account [opt]",
|
||||
"serviceUrl": "Id of source server [req]",
|
||||
"serverName": "Name of source server [jellyfin]",
|
||||
|
||||
"V POTENTIAL": "TBD V",
|
||||
"connectionEnabled": "[Toggle] boolean; enables/disables pulling data from the respective connection"
|
||||
}
|
||||
@@ -1,11 +1,5 @@
|
||||
/** @type {import('./$types').PageServerLoad} */
|
||||
export async function load({ url, fetch }) {
|
||||
const videoId = url.searchParams.get('videoId')
|
||||
const response = await fetch(`/api/yt/media?videoId=${videoId}`)
|
||||
const responseData = await response.json()
|
||||
return {
|
||||
videoId: videoId,
|
||||
videoUrl: responseData.video,
|
||||
audioUrl: responseData.audio,
|
||||
}
|
||||
}
|
||||
export const prerender = false
|
||||
export const ssr = false
|
||||
|
||||
/** @type {import('./$types').PageLoad} */
|
||||
export async function load() {}
|
||||
|
||||
@@ -1,24 +1,16 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let data
|
||||
|
||||
let videoElement
|
||||
let audioElement
|
||||
|
||||
onMount(() => {
|
||||
audioElement.volume = 0.3
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<video controls bind:this={videoElement} preload="auto">
|
||||
<source src={data.videoUrl} type="video/mp4" />
|
||||
<track kind="captions" />
|
||||
</video>
|
||||
<audio controls bind:this={audioElement}
|
||||
on:play={videoElement.play()}
|
||||
on:pause={videoElement.pause()}
|
||||
on:seeked={(videoElement.currentTime = audioElement.currentTime)} preload="auto">
|
||||
<source src={data.audioUrl} type="audio/webm" />
|
||||
</audio>
|
||||
<section class="grid h-full place-items-center">
|
||||
<div class="aspect-video h-2/3 bg-slate-900">
|
||||
<video id="video-player" controls></video>
|
||||
<!-- <iframe
|
||||
src="https://www.youtube.com/embed/uhx-u5peyeY?controls=0&autoplay=0&showinfo=0&rel=0"
|
||||
title="Video"
|
||||
frameBorder="0"
|
||||
allow="accelerometer; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
|
||||
allowFullScreen
|
||||
class="h-full w-full"
|
||||
/> -->
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user