Either google apis is the most scuffed thing ever, or it's too genius for my puny mind

This commit is contained in:
Eclypsed
2024-02-11 01:03:49 -05:00
parent 09a23fe363
commit cb03d2661b
12 changed files with 548 additions and 33 deletions

View File

@@ -0,0 +1,4 @@
import type { PageServerLoad } from '../$types'
import ytdl from 'ytdl-core'
export const load: PageServerLoad = async ({ fetch }) => {}

View File

@@ -1 +1 @@
<h1>Welcome to the Search Page!</h1>
<h1>Search Page</h1>

View File

@@ -17,7 +17,11 @@ const newConnectionSchema = z.object({
userId: z.string(),
urlOrigin: z.string().refine((val) => isValidURL(val)),
}),
accessToken: z.string(),
tokens: z.object({
accessToken: z.string(),
refreshToken: z.string().optional(),
expiry: z.number().optional(),
}),
})
export const POST: RequestHandler = async ({ params, request }) => {
@@ -28,8 +32,8 @@ export const POST: RequestHandler = async ({ params, request }) => {
const connectionValidation = newConnectionSchema.safeParse(connection)
if (!connectionValidation.success) return new Response(connectionValidation.error.message, { status: 400 })
const { service, accessToken } = connection
const newConnection = Connections.addConnection(userId, service, accessToken)
const { service, tokens } = connection
const newConnection = Connections.addConnection(userId, service, tokens)
return Response.json(newConnection)
}

View File

@@ -13,7 +13,7 @@ export const GET: RequestHandler = async ({ params, fetch }) => {
const recommendations: Song[] = []
for (const connection of userConnections) {
const { service, accessToken } = connection
const { service, tokens } = connection
switch (service.type) {
case 'jellyfin':
@@ -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()

View File

@@ -1,6 +1,8 @@
import { fail } from '@sveltejs/kit'
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
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 { google } from 'googleapis'
export const load: PageServerLoad = async ({ fetch, locals }) => {
const connectionsResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
@@ -53,10 +55,13 @@ export const actions: Actions = {
username: userData.Name,
serverName: systemData.ServerName,
}
const tokenData: Jellyfin.JFTokens = {
accessToken: authData.AccessToken,
}
const newConnectionResponse = await fetch(`/api/users/${locals.user.id}/connections`, {
method: 'POST',
headers: { apikey: SECRET_INTERNAL_API_KEY },
body: JSON.stringify({ service: serviceData, accessToken: authData.AccessToken }),
body: JSON.stringify({ service: serviceData, tokens: tokenData }),
})
if (!newConnectionResponse.ok) return fail(500, { message: 'Internal Server Error' })
@@ -64,6 +69,30 @@ export const actions: Actions = {
const newConnection: Jellyfin.JFConnection = await newConnectionResponse.json()
return { newConnection }
},
youtubeMusicLogin: async ({ request }) => {
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 { tokens } = await client.getToken(code.toString())
const tokenData: YouTubeMusic.YTTokens = {
accessToken: tokens.access_token as string,
refreshToken: tokens.refresh_token as string,
expiry: tokens.expiry_date as number,
}
const youtube = google.youtube('v3')
const userChannelResponse = await youtube.channels.list({ mine: true, part: ['id', 'snippet'], access_token: tokenData.accessToken })
const userChannel = userChannelResponse.data.items![0]
const serviceData: YouTubeMusic.YTService = {
type: 'youtube-music',
userId: userChannel.id as string,
urlOrigin: 'https://www.googleapis.com/youtube/v3',
username: userChannel.snippet?.title as string,
profilePicture: userChannel.snippet?.thumbnails?.default?.url as string | undefined,
}
},
deleteConnection: async ({ request, fetch, locals }) => {
const formData = await request.formData()
const connectionId = formData.get('connectionId')

View File

@@ -7,11 +7,13 @@
import { getDeviceUUID } from '$lib/utils'
import { SvelteComponent, type ComponentType } from 'svelte'
import ConnectionProfile from './connectionProfile.svelte'
import { enhance } from '$app/forms'
import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
export let data: PageServerData
let connections = data.userConnections
const submitCredentials: SubmitFunction = ({ formData, action, cancel }) => {
const submitCredentials: SubmitFunction = async ({ formData, action, cancel }) => {
switch (action.search) {
case '?/authenticateJellyfin':
const { serverUrl, username, password } = Object.fromEntries(formData)
@@ -30,9 +32,14 @@
const deviceId = getDeviceUUID()
formData.append('deviceId', deviceId)
break
case '?/youtubeMusicLogin':
const code = await youtubeAuthenication()
formData.append('code', code)
break
case '?/deleteConnection':
break
default:
console.log(action.search)
cancel()
}
@@ -56,11 +63,28 @@
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
}
} else if (result.type === 'redirect') {
window.open(result.location, '_blank')
}
}
}
let newConnectionModal: ComponentType<SvelteComponent<{ submitFunction: SubmitFunction }>> | null = null
const youtubeAuthenication = async (): Promise<string> => {
return new Promise((resolve) => {
// @ts-ignore (google variable is a global variable imported by html script tag)
const client = google.accounts.oauth2.initCodeClient({
client_id: PUBLIC_YOUTUBE_API_CLIENT_ID,
scope: 'https://www.googleapis.com/auth/youtube',
ux_mode: 'popup',
callback: (response: any) => {
resolve(response.code)
},
})
client.requestCode()
})
}
</script>
<main>
@@ -70,9 +94,11 @@
<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" />
</button>
<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" />
</button>
<form method="post" action="?/youtubeMusicLogin" use:enhance={submitCredentials}>
<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" />
</button>
</form>
</div>
</section>
<div class="grid gap-8">