From f9359ae3007af54a3ac3c588d8315b5fbdca50d5 Mon Sep 17 00:00:00 2001 From: Eclypsed Date: Tue, 9 Jan 2024 00:46:23 -0500 Subject: [PATCH] Created ConnectionProfile class to encapsulate connection data for display on connection page --- package-lock.json | 104 +++++++++++++----- package.json | 1 - src/lib/server/db/users.db | Bin 20480 -> 24576 bytes src/lib/server/db/users.js | 31 ++++-- src/routes/api/jellyfin/auth/+server.js | 6 +- src/routes/api/user/connections/+server.js | 39 +++---- .../settings/connections/+page.server.js | 90 ++++++++++----- src/routes/settings/connections/+page.svelte | 52 +++++---- .../connections/userDataSchemaRef.json | 12 ++ src/routes/youtube-music/+page.server.js | 16 +-- src/routes/youtube-music/+page.svelte | 34 +++--- 11 files changed, 232 insertions(+), 153 deletions(-) create mode 100644 src/routes/settings/connections/userDataSchemaRef.json diff --git a/package-lock.json b/package-lock.json index d2c5f1b..17d3600 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,6 @@ "better-sqlite3": "^9.1.1", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", - "youtubei.js": "^7.0.0", "ytdl-core": "^4.11.5" }, "devDependencies": { @@ -410,6 +409,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.0.0.tgz", "integrity": "sha512-JUFJad5lv7jxj926GPgymrWQxxjPYuJNiNjNMzqT+HiuP6Vl3dk5xzG+8sTX96np0ZAluvaMzPsjhHZ5rNuNQQ==", + "dev": true, "engines": { "node": ">=14" } @@ -468,6 +468,18 @@ "node": ">=6.0.0" } }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, "node_modules/@jridgewell/sourcemap-codec": { "version": "1.4.15", "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", @@ -666,6 +678,7 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, "bin": { "acorn": "bin/acorn" }, @@ -944,6 +957,14 @@ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==" }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1608,17 +1629,6 @@ "@types/estree": "*" } }, - "node_modules/jintr": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/jintr/-/jintr-1.1.0.tgz", - "integrity": "sha512-Tu9wk3BpN2v+kb8yT6YBtue+/nbjeLFv4vvVC4PJ7oCidHKbifWhvORrAbQfxVIQZG+67am/mDagpiGSVtvrZg==", - "funding": [ - "https://github.com/sponsors/LuanRT" - ], - "dependencies": { - "acorn": "^8.8.0" - } - }, "node_modules/jiti": { "version": "1.19.3", "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.19.3.tgz", @@ -2681,6 +2691,29 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", @@ -2878,6 +2911,34 @@ "node": ">=10" } }, + "node_modules/terser": { + "version": "5.26.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", + "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "optional": true, + "peer": true + }, "node_modules/thenify": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", @@ -2941,11 +3002,6 @@ "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", "dev": true }, - "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" - }, "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", @@ -2961,6 +3017,7 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici/-/undici-5.26.5.tgz", "integrity": "sha512-cSb4bPFd5qgR7qr2jYAi0hlX9n5YKK2ONKkLFkxl+v/9BvC0sOpZjBHDBSXc5lWAf5ty9oZdRXytBIHzgUcerw==", + "dev": true, "dependencies": { "@fastify/busboy": "^2.0.0" }, @@ -3113,19 +3170,6 @@ "node": ">= 14" } }, - "node_modules/youtubei.js": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/youtubei.js/-/youtubei.js-7.0.0.tgz", - "integrity": "sha512-z87cv6AAjj0c98BkD0qTJvBDTF2DdT+FntJUjmi+vHY2EV+CepeYQAE/eLsdhGvCb6LrNBgGVwVUzXpHYi8NoA==", - "funding": [ - "https://github.com/sponsors/LuanRT" - ], - "dependencies": { - "jintr": "^1.1.0", - "tslib": "^2.5.0", - "undici": "^5.19.1" - } - }, "node_modules/ytdl-core": { "version": "4.11.5", "resolved": "https://registry.npmjs.org/ytdl-core/-/ytdl-core-4.11.5.tgz", diff --git a/package.json b/package.json index d9476ab..77c1ae5 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,6 @@ "better-sqlite3": "^9.1.1", "joi": "^17.11.0", "jsonwebtoken": "^9.0.2", - "youtubei.js": "^7.0.0", "ytdl-core": "^4.11.5" } } diff --git a/src/lib/server/db/users.db b/src/lib/server/db/users.db index b20b4d684f199284dfc0bbbebb9cd7bbd802cf93..d780bfe7dfcd5a89e63065918e52929e083d283b 100644 GIT binary patch delta 631 zcmZozz}Rqrae_3f00RR9+e8I>aRCOsq6NJCKNwhfQyKWp_|Nh^;Z5aRy0P&+4_A{d z3%j_sHe-w7g`Nii@=OE%0(&B>`u%gmd++(SVsqokz3 zN?+g5z(CK;RL{`FM9M^Uds1wl(TLXt03l z1MUw=pcl1|wM<6)Qr9(2y8+KdcqYdjoGc_4@S2%9f7j^^ZPWk$`ttAE!u21u<;wC; zAN>L+cpw2JfCP{L5U+11j?SOGl zT)14UcTA&a>OEtpVd@t;*O;SQt)6+u?C9-|)igSX`hD|IHx7FHRtrWp%~o$i4`Bvt zD%sla>8*oCV?&4bQ)biNctrJwMyGz)=&V&r>-toJ*ftfS$4{uI_sqwj%%=mNisRWP zdOl+QII(0ywC1%_oC}?69vRXN7`guX)6A8Xm6gn!D-+-fvzSYbmvE&Ci6ei7 zRO(M*J7`%C4$K*PBO=5Z_nnDUwJ?RdzCQQC$`>mcjd>0|kBui`z}59LWzDm^dYOkr zyDF3IIy^JWKWOm40|_7jB!C2v01`j~NB{{S0VIF~kN^_+HxaNhi`vcCi!Ej!bC-zK zLVq=%TQ#|F7dD@t*TddJ(>$u5ojfW(3Lkit(W&Pd_n&;;J>z`0v};(+nz>(jRDaz5 zy8a>fm0A8(g9jc+00|%gB!C2v01`j~NB{{S0VIF~kih?lz_t0COUd63~`RXR5ii7o&h@wed_#ar%A+kDlj0ffZ?Vmt7#1=plt9X=4H3z_68AwYS|@$bghsp zly2wpw{z8AzP6n!Zs%*8m91i~TB$q%!=$(YrMRao82`NyiP1#X?@ESkmyCDmfP`*f zgbw39pV%jfm))=*u)w7a;vI$Lh_<1imin@|6>Nj~cbOQ$_$CvAc}M1m3GPra@#c=t z!x44%xa&Z*YIggprx2l7WfLSpNj&vbSDm|VTwVN-(X)xJh**Q$#C?nAz7P7TBjw{+ z8h}p(RzQcsJh$CYStX}$j+@kBB;9zIIaG3>j^#xo6964Lq{BY-6`C$hEW6m9IQ6zf zWOj?52QeKPb2(R};?+jB&0S{0<;$!k3}?tZCIX)X95OsgHA{ef4uxnX*MME74`g|q zHajl2PpH!)N3s9Wv~rzHLuRP*=tN6&)wPeQ6S^{Hl8(}I;KB5OLc)uhBW=Kf2NV)0 z@VUD`*bk1Wuioe+Y?)iL}S~_Vde&)HHyzNVO?Aa*;W~Wf3U>?{ch1vjSJL90G zf^N8O6iR4K$<~hvd<}qmQFz*%lD@qH6{~tCTj7TQA8$&Ocnuk zrk=>gIQd-$(c58xTpJNiOtVI9- diff --git a/src/lib/server/db/users.js b/src/lib/server/db/users.js index b829f5f..9571a2b 100644 --- a/src/lib/server/db/users.js +++ b/src/lib/server/db/users.js @@ -5,7 +5,7 @@ const db = new Database('./src/lib/server/db/users.db', { verbose: console.info db.pragma('foreign_keys = ON') const initUsersTable = 'CREATE TABLE IF NOT EXISTS Users(id INTEGER PRIMARY KEY AUTOINCREMENT, username VARCHAR(64) UNIQUE NOT NULL, password VARCHAR(72) NOT NULL)' const initUserConnectionsTable = - 'CREATE TABLE IF NOT EXISTS UserConnections(id INTEGER PRIMARY KEY AUTOINCREMENT, userId INTEGER NOT NULL, serviceType VARCHAR(64) NOT NULL, accessToken TEXT, refreshToken TEXT, expiry INTEGER, connectionInfo TEXT, FOREIGN KEY(userId) REFERENCES Users(id))' + 'CREATE TABLE IF NOT EXISTS UserConnections(id VARCHAR(36) PRIMARY KEY, userId INTEGER NOT NULL, serviceType VARCHAR(64) NOT NULL, serviceUserId TEXT NOT NULL, serviceUrl TEXT NOT NULL, accessToken TEXT NOT NULL, refreshToken TEXT, expiry INTEGER, FOREIGN KEY(userId) REFERENCES Users(id))' db.exec(initUsersTable) db.exec(initUserConnectionsTable) @@ -27,29 +27,38 @@ export class Users { export class UserConnections { static validServices = Object.keys(Services) - static getConnections = (userId) => { + static getConnection = (id) => { + return db.prepare(`SELECT * FROM UserConnections WHERE id = ?`).get(id) + } + + static getUserConnections = (userId) => { const connections = db.prepare(`SELECT * FROM UserConnections WHERE userId = ?`).all(userId) if (connections.length === 0) return null return connections } // May want to give accessToken a default of null in the future if one of the services does not use access tokens - static addConnection = (userId, serviceType, accessToken, options = {}) => { - const { refreshToken = null, expiry = null, connectionInfo = null } = options + static addConnection = (userId, serviceType, serviceUserId, serviceUrl, accessToken, additionalApiData = {}) => { + const { refreshToken = null, expiry = null } = additionalApiData if (!this.validServices.includes(serviceType)) throw new Error(`Service name ${serviceType} is invalid`) - if (connectionInfo) JSON.parse(connectionInfo) // Aditional validation, if connectionInfo is not stringified valid json it will throw an error - - const commandInfo = db - .prepare('INSERT INTO UserConnections(userId, serviceType, accessToken, refreshToken, expiry, connectionInfo) VALUES(?, ?, ?, ?, ?, ?)') - .run(userId, serviceType, accessToken, refreshToken, expiry, connectionInfo) - return commandInfo.lastInsertRowid + const connectionId = '10000000-1000-4000-8000-100000000000'.replace(/[018]/g, (c) => (c ^ (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4)))).toString(16)) + db.prepare('INSERT INTO UserConnections(id, userId, serviceType, serviceUserId, serviceUrl, accessToken, refreshToken, expiry) VALUES(?, ?, ?, ?, ?, ?, ?, ?)').run( + connectionId, + userId, + serviceType, + serviceUserId, + serviceUrl, + accessToken, + refreshToken, + expiry, + ) + return connectionId } static deleteConnection = (userId, serviceId) => { const commandInfo = db.prepare('DELETE FROM UserConnections WHERE userId = ? AND id = ?').run(userId, serviceId) if (!commandInfo.changes === 0) throw new Error(`User does not have connection with id: ${serviceId}`) - return serviceId } } diff --git a/src/routes/api/jellyfin/auth/+server.js b/src/routes/api/jellyfin/auth/+server.js index 5c08955..71395d0 100644 --- a/src/routes/api/jellyfin/auth/+server.js +++ b/src/routes/api/jellyfin/auth/+server.js @@ -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({ diff --git a/src/routes/api/user/connections/+server.js b/src/routes/api/user/connections/+server.js index 578ec6f..35882a2 100644 --- a/src/routes/api/user/connections/+server.js +++ b/src/routes/api/user/connections/+server.js @@ -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 }) } diff --git a/src/routes/settings/connections/+page.server.js b/src/routes/settings/connections/+page.server.js index 8f1b567..547201e 100644 --- a/src/routes/settings/connections/+page.server.js +++ b/src/routes/settings/connections/+page.server.js @@ -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 } }, } diff --git a/src/routes/settings/connections/+page.svelte b/src/routes/settings/connections/+page.svelte index 7e1ddea..3575644 100644 --- a/src/routes/settings/connections/+page.svelte +++ b/src/routes/settings/connections/+page.svelte @@ -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 @@
- {#each Object.entries(existingConnections) as [connectionId, connectionData]} - {@const serviceData = Services[connectionData.serviceType]} + {#each connectionProfiles as connectionProfile} + {@const serviceData = Services[connectionProfile.serviceType]}
{serviceData.displayName} icon
- {#if connectionData.serviceType === 'jellyfin'} -
{connectionData.connectionInfo.User.Name}
- {:else} -
Account Name
- {/if} -
{serviceData.displayName}
+
{connectionProfile?.username ? connectionProfile.username : 'Placeholder Account Name'}
+
+ {serviceData.displayName} + {#if connectionProfile.serviceType === 'jellyfin'} + - {connectionProfile.serverName} + {/if} +
- (modal = `delete-${connectionId}`)}> + (modal = `delete-${connectionProfile.connectionId}`)}>
@@ -113,11 +116,12 @@
{#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]}
-

Delete {service.displayName} connection?

+

Delete {serviceData.displayName} connection?

- +
diff --git a/src/routes/settings/connections/userDataSchemaRef.json b/src/routes/settings/connections/userDataSchemaRef.json new file mode 100644 index 0000000..db222ed --- /dev/null +++ b/src/routes/settings/connections/userDataSchemaRef.json @@ -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" +} diff --git a/src/routes/youtube-music/+page.server.js b/src/routes/youtube-music/+page.server.js index 366f011..de2bb88 100644 --- a/src/routes/youtube-music/+page.server.js +++ b/src/routes/youtube-music/+page.server.js @@ -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() {} diff --git a/src/routes/youtube-music/+page.svelte b/src/routes/youtube-music/+page.svelte index 3e087b2..7107e07 100644 --- a/src/routes/youtube-music/+page.svelte +++ b/src/routes/youtube-music/+page.svelte @@ -1,24 +1,16 @@ - - \ No newline at end of file +
+
+ + +
+