FINALLY, Beautiful connection types!
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Jellyfin, YouTubeMusic } from '$lib/services'
|
||||
import { Connections } from '$lib/server/users'
|
||||
|
||||
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[] = []
|
||||
for (const connectionId of ids) connections.push(Connections.getConnection(connectionId))
|
||||
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':
|
||||
connection.service = await YouTubeMusic.fetchServiceInfo(connection.service.userId, connection.tokens.accessToken)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ connections })
|
||||
}
|
||||
|
||||
@@ -1,53 +0,0 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/users'
|
||||
import { google } from 'googleapis'
|
||||
|
||||
const youtubeInfo = async (connection: Connection): Promise<ConnectionInfo> => {
|
||||
const youtube = google.youtube('v3')
|
||||
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['snippet'], access_token: connection.accessToken })
|
||||
const userChannel = userChannelResponse.data.items![0]
|
||||
|
||||
return {
|
||||
connectionId: connection.id,
|
||||
serviceType: connection.service.type,
|
||||
username: userChannel.snippet?.title as string,
|
||||
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined,
|
||||
}
|
||||
}
|
||||
|
||||
const jellyfinInfo = async (connection: Connection): Promise<ConnectionInfo> => {
|
||||
const reqHeaders = new Headers({ Authorization: `MediaBrowser Token="${connection.accessToken}"` })
|
||||
|
||||
const userUrl = new URL(`Users/${connection.service.userId}`, connection.service.urlOrigin).href
|
||||
const systemUrl = new URL('System/Info', connection.service.urlOrigin).href
|
||||
|
||||
const userResponse = await fetch(userUrl, { headers: reqHeaders })
|
||||
const systemResponse = await fetch(systemUrl, { headers: reqHeaders })
|
||||
|
||||
const userData: Jellyfin.User = await userResponse.json()
|
||||
const systemData: Jellyfin.System = await systemResponse.json()
|
||||
|
||||
return {
|
||||
connectionId: connection.id,
|
||||
serviceType: 'jellyfin',
|
||||
username: userData.Name,
|
||||
serverName: systemData.ServerName,
|
||||
}
|
||||
}
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const connectionId = params.connectionId!
|
||||
const connection = Connections.getConnection(connectionId)
|
||||
|
||||
let info: ConnectionInfo
|
||||
switch (connection.service.type) {
|
||||
case 'jellyfin':
|
||||
info = await jellyfinInfo(connection)
|
||||
break
|
||||
case 'youtube-music':
|
||||
info = await youtubeInfo(connection)
|
||||
break
|
||||
}
|
||||
|
||||
return Response.json({ info })
|
||||
}
|
||||
@@ -1,9 +1,21 @@
|
||||
import { Connections } from '$lib/server/users'
|
||||
import { Jellyfin, YouTubeMusic } from '$lib/services'
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
|
||||
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':
|
||||
connection.service = await YouTubeMusic.fetchServiceInfo(connection.service.userId, connection.tokens.accessToken)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return Response.json({ connections })
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import { Jellyfin } from '$lib/service-managers/jellyfin'
|
||||
import { Jellyfin } from '$lib/services'
|
||||
|
||||
// This is temporary functionally for the sake of developing the app.
|
||||
// In the future will implement more robust algorithm for offering recommendations
|
||||
@@ -13,9 +13,9 @@ export const GET: RequestHandler = async ({ params, fetch }) => {
|
||||
const recommendations: MediaItem[] = []
|
||||
|
||||
for (const connection of userConnections.connections) {
|
||||
const { service, accessToken } = connection as Connection
|
||||
const { type, service, tokens } = connection as Connection<serviceType>
|
||||
|
||||
switch (service.type) {
|
||||
switch (type) {
|
||||
case 'jellyfin':
|
||||
const mostPlayedSongsSearchParams = new URLSearchParams({
|
||||
SortBy: 'PlayCount',
|
||||
@@ -26,7 +26,7 @@ export const GET: RequestHandler = async ({ params, fetch }) => {
|
||||
})
|
||||
|
||||
const mostPlayedSongsURL = new URL(`/Users/${service.userId}/Items?${mostPlayedSongsSearchParams.toString()}`, service.urlOrigin).href
|
||||
const requestHeaders = new Headers({ Authorization: `MediaBrowser Token="${accessToken}"` })
|
||||
const requestHeaders = new Headers({ Authorization: `MediaBrowser Token="${tokens.accessToken}"` })
|
||||
|
||||
const mostPlayedResponse = await fetch(mostPlayedSongsURL, { headers: requestHeaders })
|
||||
const mostPlayedData = await mostPlayedResponse.json()
|
||||
|
||||
@@ -18,7 +18,7 @@ export const actions: Actions = {
|
||||
const user = Users.getUsername(username.toString())
|
||||
if (!user) return fail(400, { message: 'Invalid Username' })
|
||||
|
||||
const passwordValid = await compare(password.toString(), user.password)
|
||||
const passwordValid = await compare(password.toString(), user.passwordHash)
|
||||
if (!passwordValid) return fail(400, { message: 'Invalid Password' })
|
||||
|
||||
const authToken = jwt.sign({ id: user.id, username: user.username }, SECRET_JWT_KEY, { expiresIn: '100d' })
|
||||
|
||||
@@ -43,13 +43,11 @@ export const actions: Actions = {
|
||||
return fail(400, { message: 'Could not reach Jellyfin server' })
|
||||
}
|
||||
|
||||
const serviceData: Service = {
|
||||
type: 'jellyfin',
|
||||
userId: authData.User.Id,
|
||||
urlOrigin: serverUrl.toString(),
|
||||
}
|
||||
|
||||
const newConnection = Connections.addConnection(locals.user.id, serviceData, authData.AccessToken)
|
||||
const newConnection = Connections.addConnection('jellyfin', {
|
||||
userId: locals.user.id,
|
||||
service: { userId: authData.User.Id, urlOrigin: serverUrl.toString() },
|
||||
tokens: { accessToken: authData.AccessToken },
|
||||
})
|
||||
|
||||
return { newConnection }
|
||||
},
|
||||
@@ -63,13 +61,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 serviceData: Service = {
|
||||
type: 'youtube-music',
|
||||
userId: userChannel.id!,
|
||||
urlOrigin: 'https://www.googleapis.com/youtube/v3',
|
||||
}
|
||||
|
||||
const newConnection = Connections.addConnection(locals.user.id, serviceData, tokens.access_token!, tokens.refresh_token!, tokens.expiry_date!)
|
||||
const newConnection = Connections.addConnection('youtube-music', {
|
||||
userId: locals.user.id,
|
||||
service: { userId: userChannel.id! },
|
||||
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
|
||||
})
|
||||
|
||||
return { newConnection }
|
||||
},
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
import Services from '$lib/services.json'
|
||||
import { serviceData } from '$lib/services'
|
||||
import JellyfinAuthBox from './jellyfinAuthBox.svelte'
|
||||
import { newestAlert } from '$lib/stores.js'
|
||||
import type { PageServerData } from './$types.js'
|
||||
@@ -11,7 +11,7 @@
|
||||
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||
|
||||
export let data: PageServerData
|
||||
let connections: Connection[] = data.connections
|
||||
let connections: Connection<serviceType>[] = data.connections
|
||||
|
||||
const authenticateJellyfin: SubmitFunction = ({ formData, cancel }) => {
|
||||
const { serverUrl, username, password } = Object.fromEntries(formData)
|
||||
@@ -34,11 +34,11 @@
|
||||
if (result.type === 'failure') {
|
||||
return ($newestAlert = ['warning', result.data?.message])
|
||||
} else if (result.type === 'success') {
|
||||
const newConnection: Connection = result.data!.newConnection
|
||||
const newConnection: Connection<'jellyfin'> = result.data!.newConnection
|
||||
connections = [...connections, newConnection]
|
||||
|
||||
newConnectionModal = null
|
||||
return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`])
|
||||
return ($newestAlert = ['success', `Added ${serviceData[newConnection.type].displayName}`])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -67,7 +67,7 @@
|
||||
if (result.type === 'failure') {
|
||||
return ($newestAlert = ['warning', result.data?.message])
|
||||
} else if (result.type === 'success') {
|
||||
const newConnection: Connection = result.data!.newConnection
|
||||
const newConnection: Connection<'youtube-music'> = result.data!.newConnection
|
||||
connections = [...connections, newConnection]
|
||||
return ($newestAlert = ['success', 'Added Youtube Music'])
|
||||
}
|
||||
@@ -84,12 +84,12 @@
|
||||
} else if (result.type === 'success') {
|
||||
const id = result.data!.deletedConnectionId
|
||||
const indexToDelete = connections.findIndex((connection) => connection.id === id)
|
||||
const serviceType = connections[indexToDelete].service.type
|
||||
const serviceType = connections[indexToDelete].type
|
||||
|
||||
connections.splice(indexToDelete, 1)
|
||||
connections = connections
|
||||
|
||||
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
|
||||
return ($newestAlert = ['success', `Deleted ${serviceData[serviceType].displayName}`])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -102,11 +102,11 @@
|
||||
<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={Services.jellyfin.icon} alt="{Services.jellyfin.displayName} icon" class="aspect-square h-full p-2" />
|
||||
<img src={serviceData.jellyfin.icon} alt="{serviceData.jellyfin.displayName} icon" class="aspect-square h-full p-2" />
|
||||
</button>
|
||||
<form method="post" action="?/youtubeMusicLogin" use:enhance={authenticateYouTube}>
|
||||
<button class="add-connection-button h-14 rounded-md">
|
||||
<img src={Services['youtube-music'].icon} alt="{Services['youtube-music'].displayName} icon" class="aspect-square h-full p-2" />
|
||||
<img src={serviceData['youtube-music'].icon} alt="{serviceData['youtube-music'].displayName} icon" class="aspect-square h-full p-2" />
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,47 +1,43 @@
|
||||
<script lang="ts">
|
||||
import Services from '$lib/services.json'
|
||||
import { serviceData } from '$lib/services'
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import Toggle from '$lib/components/util/toggle.svelte'
|
||||
import type { SubmitFunction } from '@sveltejs/kit'
|
||||
import { fly } from 'svelte/transition'
|
||||
import { enhance } from '$app/forms'
|
||||
|
||||
export let connection: Connection
|
||||
export let connection: Connection<serviceType>
|
||||
export let submitFunction: SubmitFunction
|
||||
|
||||
$: serviceData = Services[connection.service.type]
|
||||
$: reactiveServiceData = serviceData[connection.type]
|
||||
|
||||
let showModal = false
|
||||
</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="h-full aspect-square p-1 relative">
|
||||
<img src={serviceData.icon} alt="{serviceData.displayName} icon" />
|
||||
<div class="relative aspect-square h-full p-1">
|
||||
<img src={reactiveServiceData.icon} alt="{reactiveServiceData.displayName} icon" />
|
||||
{#if 'profilePicture' in connection.service && typeof connection.service.profilePicture === 'string'}
|
||||
<img src="{connection.service.profilePicture}" alt="" class="absolute h-5 aspect-square bottom-0 right-0 rounded-full"/>
|
||||
<img src={connection.service.profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div>Username</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
{serviceData.displayName}
|
||||
{reactiveServiceData.displayName}
|
||||
{#if 'serverName' in connection.service}
|
||||
- {connection.service.serverName}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
<div class="relative ml-auto h-8 flex flex-row-reverse gap-2">
|
||||
<IconButton halo={true} on:click={() => showModal = !showModal}>
|
||||
<div class="relative ml-auto flex h-8 flex-row-reverse gap-2">
|
||||
<IconButton halo={true} on:click={() => (showModal = !showModal)}>
|
||||
<i slot="icon" class="fa-solid fa-ellipsis-vertical text-xl text-neutral-500" />
|
||||
</IconButton>
|
||||
{#if showModal}
|
||||
<form
|
||||
use:enhance={submitFunction}
|
||||
method="post"
|
||||
class="absolute right-0 top-full flex flex-col items-center justify-center gap-1 rounded-md bg-neutral-900 p-2 text-xs"
|
||||
>
|
||||
<button formaction="?/deleteConnection" class="py-2 px-3 whitespace-nowrap hover:bg-neutral-800 rounded-md">
|
||||
<form use:enhance={submitFunction} method="post" class="absolute right-0 top-full flex flex-col items-center justify-center gap-1 rounded-md bg-neutral-900 p-2 text-xs">
|
||||
<button formaction="?/deleteConnection" class="whitespace-nowrap rounded-md px-3 py-2 hover:bg-neutral-800">
|
||||
<i class="fa-solid fa-link-slash mr-1" />
|
||||
Delete Connection
|
||||
</button>
|
||||
|
||||
Reference in New Issue
Block a user