Going to try out some OOP/DI patterns and see where that takes me

This commit is contained in:
Eclypsed
2024-03-24 16:03:31 -04:00
parent d50497e7d5
commit 15db7f1aed
22 changed files with 894 additions and 900 deletions

View File

@@ -1,25 +1,16 @@
import type { RequestHandler } from '@sveltejs/kit'
import { Jellyfin } from '$lib/services'
import { YouTubeMusic } from '$lib/service-managers/youtube-music'
import { Connections } from '$lib/server/users'
import { Connections } from '$lib/server/connections'
export const GET: RequestHandler = async ({ url }) => {
const ids = url.searchParams.get('ids')?.replace(/\s/g, '').split(',')
if (!ids) return new Response('Missing ids query parameter', { status: 400 })
const connections: Connection<serviceType>[] = []
for (const connectionId of ids) {
const connection = Connections.getConnection(connectionId)
switch (connection.type) {
case 'jellyfin':
connection.service = await Jellyfin.fetchSerivceInfo(connection.service.userId, connection.service.urlOrigin, connection.tokens.accessToken)
break
case 'youtube-music':
const ytmusic = new YouTubeMusic(connection)
connection.service = await ytmusic.fetchServiceInfo()
break
}
connections.push(connection)
const connections: ConnectionInfo[] = []
for (const connection of Connections.getConnections(ids)) {
await connection
.getConnectionInfo()
.then((info) => connections.push(info))
.catch((reason) => console.log(`Failed to fetch connection info: ${reason}`))
}
return Response.json({ connections })

View File

@@ -1,22 +1,15 @@
import { Connections } from '$lib/server/users'
import { Jellyfin } from '$lib/services'
import { YouTubeMusic } from '$lib/service-managers/youtube-music'
import type { RequestHandler } from '@sveltejs/kit'
import { Connections } from '$lib/server/connections'
export const GET: RequestHandler = async ({ params }) => {
const userId = params.userId!
const connections = Connections.getUserConnections(userId)
for (const connection of connections) {
switch (connection.type) {
case 'jellyfin':
connection.service = await Jellyfin.fetchSerivceInfo(connection.service.userId, connection.service.urlOrigin, connection.tokens.accessToken)
break
case 'youtube-music':
const youTubeMusic = new YouTubeMusic(connection)
connection.service = await youTubeMusic.fetchServiceInfo()
break
}
const connections: ConnectionInfo[] = []
for (const connection of Connections.getUserConnections(userId)) {
await connection
.getConnectionInfo()
.then((info) => connections.push(info))
.catch((reason) => console.log(`Failed to fetch connection info: ${reason}`))
}
return Response.json({ connections })

View File

@@ -1,49 +1,17 @@
import type { RequestHandler } from '@sveltejs/kit'
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
import { Jellyfin } from '$lib/services'
import { YouTubeMusic } from '$lib/service-managers/youtube-music'
import { Connections } from '$lib/server/connections'
// 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, fetch }) => {
export const GET: RequestHandler = async ({ params }) => {
const userId = params.userId!
const connectionsResponse = await fetch(`/api/users/${userId}/connections`, { headers: { apikey: SECRET_INTERNAL_API_KEY } })
const userConnections = await connectionsResponse.json()
const recommendations: MediaItem[] = []
for (const connection of userConnections.connections) {
const { type, service, tokens } = connection as Connection<serviceType>
switch (type) {
case 'jellyfin':
const mostPlayedSongsSearchParams = new URLSearchParams({
SortBy: 'PlayCount',
SortOrder: 'Descending',
IncludeItemTypes: 'Audio',
Recursive: 'true',
limit: '10',
})
const mostPlayedSongsURL = new URL(`/Users/${service.userId}/Items?${mostPlayedSongsSearchParams.toString()}`, service.urlOrigin).href
const requestHeaders = new Headers({ Authorization: `MediaBrowser Token="${tokens.accessToken}"` })
const mostPlayedResponse = await fetch(mostPlayedSongsURL, { headers: requestHeaders })
const mostPlayedData = await mostPlayedResponse.json()
for (const song of mostPlayedData.Items) recommendations.push(Jellyfin.songFactory(song, connection))
break
case 'youtube-music':
const youtubeMusic = new YouTubeMusic(connection)
await youtubeMusic
.getHome()
.then(({ listenAgain, quickPicks, newReleases }) => {
for (const mediaItem of listenAgain) recommendations.push(mediaItem)
})
.catch()
break
}
for (const connection of Connections.getUserConnections(userId)) {
await connection
.getRecommendations()
.then((connectionRecommendations) => recommendations.push(...connectionRecommendations))
.catch((reason) => console.log(`Failed to fetch recommendations: ${reason}`))
}
return Response.json({ recommendations })

View File

@@ -2,7 +2,7 @@ import { SECRET_JWT_KEY } from '$env/static/private'
import { fail, redirect } from '@sveltejs/kit'
import { compare, hash } from 'bcrypt-ts'
import type { PageServerLoad, Actions } from './$types'
import { Users } from '$lib/server/users'
import { DB } from '$lib/server/db'
import jwt from 'jsonwebtoken'
export const load: PageServerLoad = async ({ url }) => {
@@ -15,7 +15,7 @@ export const actions: Actions = {
const formData = await request.formData()
const { username, password, redirectLocation } = Object.fromEntries(formData)
const user = Users.getUsername(username.toString())
const user = DB.getUsername(username.toString())
if (!user) return fail(400, { message: 'Invalid Username' })
const passwordValid = await compare(password.toString(), user.passwordHash)
@@ -34,7 +34,7 @@ export const actions: Actions = {
const { username, password } = Object.fromEntries(formData)
const passwordHash = await hash(password.toString(), 10)
const newUser = Users.addUser(username.toString(), passwordHash)
const newUser = DB.addUser(username.toString(), passwordHash)
if (!newUser) return fail(400, { message: 'Username already in use' })
const authToken = jwt.sign({ id: newUser.id, username: newUser.username }, SECRET_JWT_KEY, { expiresIn: '100d' })

View File

@@ -2,7 +2,8 @@ import { fail } from '@sveltejs/kit'
import { SECRET_INTERNAL_API_KEY, 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 { Connections } from '$lib/server/users'
import { DB } from '$lib/server/db'
import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
import { google } from 'googleapis'
export const load: PageServerLoad = async ({ fetch, locals }) => {
@@ -21,38 +22,22 @@ export const actions: Actions = {
const formData = await request.formData()
const { serverUrl, username, password, deviceId } = Object.fromEntries(formData)
const authUrl = new URL('/Users/AuthenticateByName', serverUrl.toString()).href
let authData: Jellyfin.AuthData
try {
const authResponse = await fetch(authUrl, {
method: 'POST',
body: JSON.stringify({
Username: username,
Pw: password,
}),
headers: {
'Content-Type': 'application/json; charset=utf-8',
'X-Emby-Authorization': `MediaBrowser Client="Lazuli", Device="Chrome", DeviceId="${deviceId}", Version="1.0.0.0"`,
},
})
if (!URL.canParse(serverUrl.toString())) return fail(400, { message: 'Invalid Server URL' })
if (!authResponse.ok) return fail(401, { message: 'Failed to authenticate' })
const authData = await Jellyfin.authenticateByName(username.toString(), password.toString(), new URL(serverUrl.toString()), deviceId.toString()).catch((error: JellyfinFetchError) => error)
authData = await authResponse.json()
} catch {
return fail(400, { message: 'Could not reach Jellyfin server' })
}
if (authData instanceof JellyfinFetchError) return fail(authData.httpCode, { message: authData.message })
const newConnectionId = Connections.addConnection('jellyfin', locals.user.id, { userId: authData.User.Id, urlOrigin: serverUrl.toString() }, { accessToken: authData.AccessToken })
const newConnectionId = DB.addConnectionInfo(locals.user.id, { type: 'jellyfin', serviceInfo: { userId: authData.User.Id, urlOrigin: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
method: 'GET',
headers: { apikey: SECRET_INTERNAL_API_KEY },
}).then((response) => {
return response.json()
})
const responseData = await response.json()
return { newConnection: responseData.connections[0] }
return { newConnection: response.connections[0] }
},
youtubeMusicLogin: async ({ request, fetch, locals }) => {
const formData = await request.formData()
@@ -64,12 +49,11 @@ export const actions: Actions = {
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['id', 'snippet'], access_token: tokens.access_token! })
const userChannel = userChannelResponse.data.items![0]
const newConnectionId = Connections.addConnection(
'youtube-music',
locals.user.id,
{ userId: userChannel.id! },
{ accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
)
const newConnectionId = DB.addConnectionInfo(locals.user.id, {
type: 'youtube-music',
serviceInfo: { userId: userChannel.id! },
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
})
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
method: 'GET',
@@ -84,7 +68,7 @@ export const actions: Actions = {
const formData = await request.formData()
const connectionId = formData.get('connectionId')!.toString()
Connections.deleteConnection(connectionId)
DB.deleteConnectionInfo(connectionId)
return { deletedConnectionId: connectionId }
},

View File

@@ -11,7 +11,7 @@
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
export let data: PageServerData
let connections: Connection<serviceType>[] = data.connections
let connections: ConnectionInfo[] = data.connections
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
const { serverUrl, username, password } = Object.fromEntries(formData)
@@ -34,7 +34,7 @@
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: Connection<'jellyfin'> = result.data!.newConnection
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
newConnectionModal = null
@@ -67,7 +67,7 @@
if (result.type === 'failure') {
return ($newestAlert = ['warning', result.data?.message])
} else if (result.type === 'success') {
const newConnection: Connection<'youtube-music'> = result.data!.newConnection
const newConnection: ConnectionInfo = result.data!.newConnection
connections = [...connections, newConnection]
return ($newestAlert = ['success', 'Added Youtube Music'])
}

View File

@@ -6,7 +6,7 @@
import { fly } from 'svelte/transition'
import { enhance } from '$app/forms'
export let connection: Connection<serviceType>
export let connection: ConnectionInfo
export let submitFunction: SubmitFunction
$: serviceData = Services[connection.type]
@@ -14,16 +14,16 @@
let showModal = false
const subHeaderItems: string[] = []
if ('username' in connection.service && connection.service.username) subHeaderItems.push(connection.service.username)
if ('serverName' in connection.service && connection.service.serverName) subHeaderItems.push(connection.service.serverName)
if ('username' in connection.serviceInfo && connection.serviceInfo.username) subHeaderItems.push(connection.serviceInfo.username)
if ('serverName' in connection.serviceInfo && connection.serviceInfo.serverName) subHeaderItems.push(connection.serviceInfo.serverName)
</script>
<section class="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">
<div class="relative aspect-square h-full p-1">
<img src={serviceData.icon} alt="{serviceData.displayName} icon" />
{#if 'profilePicture' in connection.service && connection.service.profilePicture}
<img src={connection.service.profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
{#if 'profilePicture' in connection.serviceInfo && connection.serviceInfo.profilePicture}
<img src={connection.serviceInfo.profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
{/if}
</div>
<div>