ConnectionInfo and the db ConnectionRow types are now completely seperate things. Started on audio fetching yay!
This commit is contained in:
@@ -1,9 +1,10 @@
|
||||
<script lang="ts">
|
||||
import { pageWidth } from '$lib/stores'
|
||||
import SearchBar from '$lib/components/util/searchBar.svelte'
|
||||
import type { LayoutData } from './$types'
|
||||
import { currentlyPlaying } from '$lib/stores'
|
||||
import NavTab from '$lib/components/navbar/navTab.svelte'
|
||||
import PlaylistTab from '$lib/components/navbar/playlistTab.svelte'
|
||||
import MediaPlayer from '$lib/components/media/mediaPlayer.svelte'
|
||||
|
||||
export let data: LayoutData
|
||||
|
||||
@@ -21,50 +22,32 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if $pageWidth >= 768}
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="no-scrollbar fixed left-0 top-0 z-10 grid h-full w-20 grid-cols-1 grid-rows-[min-content_auto] gap-5 px-3 py-12">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each data.navTabs as nav}
|
||||
<NavTab {nav} disabled={inPathnameHeirarchy(data.url.pathname, nav.pathname)} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="no-scrollbar flex flex-col gap-5 overflow-y-scroll px-1.5">
|
||||
{#each data.playlistTabs as playlist}
|
||||
<PlaylistTab {playlist} on:mouseenter={(event) => setTooltip(event.detail.x, event.detail.y, event.detail.content)} on:mouseleave={() => (playlistTooltip.style.display = 'none')} />
|
||||
{/each}
|
||||
</div>
|
||||
<div bind:this={playlistTooltip} class="absolute hidden max-w-48 -translate-y-1/2 translate-x-10 whitespace-nowrap rounded bg-neutral-800 px-2 py-1.5 text-sm">
|
||||
<div class="overflow-clip text-ellipsis">PLAYLIST_NAME</div>
|
||||
<div class="overflow-clip text-ellipsis text-neutral-400">Playlist • {data.user.username}</div>
|
||||
</div>
|
||||
<div class="h-full overflow-hidden">
|
||||
<div class="no-scrollbar fixed left-0 top-0 z-10 grid h-full w-20 grid-cols-1 grid-rows-[min-content_auto] gap-5 px-3 py-12">
|
||||
<div class="flex flex-col gap-4">
|
||||
{#each data.navTabs as nav}
|
||||
<NavTab {nav} disabled={inPathnameHeirarchy(data.url.pathname, nav.pathname)} />
|
||||
{/each}
|
||||
</div>
|
||||
<div class="no-scrollbar flex flex-col gap-5 overflow-y-scroll px-1.5">
|
||||
{#each data.playlistTabs as playlist}
|
||||
<PlaylistTab {playlist} on:mouseenter={(event) => setTooltip(event.detail.x, event.detail.y, event.detail.content)} on:mouseleave={() => (playlistTooltip.style.display = 'none')} />
|
||||
{/each}
|
||||
</div>
|
||||
<div bind:this={playlistTooltip} class="absolute hidden max-w-48 -translate-y-1/2 translate-x-10 whitespace-nowrap rounded bg-neutral-800 px-2 py-1.5 text-sm">
|
||||
<div class="overflow-clip text-ellipsis">PLAYLIST_NAME</div>
|
||||
<div class="overflow-clip text-ellipsis text-neutral-400">Playlist • {data.user.username}</div>
|
||||
</div>
|
||||
<section class="no-scrollbar overflow-y-scroll px-[max(7rem,_7vw)]">
|
||||
<div class="my-6 max-w-xl">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
<footer class="fixed bottom-0 flex w-full flex-col items-center justify-center">
|
||||
<!-- <MiniPlayer displayMode={'horizontal'} /> -->
|
||||
</footer>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="h-full overflow-hidden">
|
||||
<section class="no-scrollbar h-full overflow-y-scroll px-[5vw] pt-16">
|
||||
<slot />
|
||||
</section>
|
||||
<footer class="fixed bottom-0 flex w-full flex-col items-center justify-center">
|
||||
<!-- <MiniPlayer displayMode={'vertical'} /> -->
|
||||
<!-- <NavbarFoot
|
||||
{currentPathname}
|
||||
transitionTime={pageTransitionTime}
|
||||
on:navigate={(event) => {
|
||||
event.detail.direction === 'right' ? (directionMultiplier = 1) : (directionMultiplier = -1)
|
||||
currentPathname = event.detail.pathname
|
||||
goto(currentPathname)
|
||||
}}
|
||||
/> -->
|
||||
</footer>
|
||||
</div>
|
||||
{/if}
|
||||
<section class="no-scrollbar overflow-y-scroll px-[max(7rem,_7vw)]">
|
||||
<div class="my-6 max-w-xl">
|
||||
<SearchBar />
|
||||
</div>
|
||||
<slot />
|
||||
</section>
|
||||
<section class="absolute bottom-0 z-40 max-h-full w-full">
|
||||
{#if $currentlyPlaying}
|
||||
<MediaPlayer currentlyPlaying={$currentlyPlaying} />
|
||||
{/if}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
@@ -2,9 +2,11 @@ import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ locals, fetch }) => {
|
||||
const recommendationResponse = await fetch(`/api/users/${locals.user.id}/recommendations`, { headers: { apikey: SECRET_INTERNAL_API_KEY } })
|
||||
const recommendationData = await recommendationResponse.json()
|
||||
const { recommendations } = recommendationData
|
||||
const recommendationResponse = await fetch(`/api/users/${locals.user.id}/recommendations`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())
|
||||
|
||||
const recommendations: (Song | Album | Artist | Playlist)[] = recommendationResponse.recommendations
|
||||
|
||||
return { recommendations }
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
<script lang="ts">
|
||||
import ScrollableCardMenu from '$lib/components/media/scrollableCardMenu.svelte'
|
||||
import MediaCard from '$lib/components/media/mediaCard.svelte'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
|
||||
@@ -4,7 +4,6 @@ import { PUBLIC_YOUTUBE_API_CLIENT_ID } from '$env/static/public'
|
||||
import type { PageServerLoad, Actions } from './$types'
|
||||
import { DB } from '$lib/server/db'
|
||||
import { Jellyfin, JellyfinFetchError } from '$lib/server/jellyfin'
|
||||
import type { ConnectionInfo } from '$lib/server/connections'
|
||||
import { google } from 'googleapis'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, locals }) => {
|
||||
@@ -29,7 +28,7 @@ export const actions: Actions = {
|
||||
|
||||
if (authData instanceof JellyfinFetchError) return fail(authData.httpCode, { message: authData.message })
|
||||
|
||||
const newConnectionId = DB.addConnectionInfo({ userId: locals.user.id, type: 'jellyfin', service: { userId: authData.User.Id, urlOrigin: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
|
||||
const newConnectionId = DB.addConnectionInfo({ userId: locals.user.id, type: 'jellyfin', service: { userId: authData.User.Id, serverUrl: serverUrl.toString() }, tokens: { accessToken: authData.AccessToken } })
|
||||
|
||||
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
|
||||
method: 'GET',
|
||||
|
||||
@@ -134,15 +134,8 @@
|
||||
</div>
|
||||
</section>
|
||||
<div id="connection-profile-grid" class="grid gap-8">
|
||||
{#each connections as connection}
|
||||
<ConnectionProfile
|
||||
id={connection.id}
|
||||
type={connection.type}
|
||||
username={connection.service.username}
|
||||
profilePicture={'profilePicture' in connection.service ? connection.service.profilePicture : undefined}
|
||||
serverName={'serverName' in connection.service ? connection.service.serverName : undefined}
|
||||
submitFunction={profileActions}
|
||||
/>
|
||||
{#each connections as connectionInfo}
|
||||
<ConnectionProfile {connectionInfo} submitFunction={profileActions} />
|
||||
{/each}
|
||||
</div>
|
||||
{#if newConnectionModal !== null}
|
||||
|
||||
@@ -6,26 +6,32 @@
|
||||
import { fly } from 'svelte/transition'
|
||||
import { enhance } from '$app/forms'
|
||||
|
||||
export let id: string, type: 'jellyfin' | 'youtube-music', username: string | undefined, profilePicture: string | undefined, serverName: string | undefined
|
||||
export let connectionInfo: ConnectionInfo
|
||||
export let submitFunction: SubmitFunction
|
||||
|
||||
$: serviceData = Services[type]
|
||||
$: serviceData = Services[connectionInfo.type]
|
||||
|
||||
let showModal = false
|
||||
|
||||
const subHeaderItems = [username, serverName]
|
||||
const subHeaderItems: string[] = []
|
||||
if ('username' in connectionInfo) {
|
||||
subHeaderItems.push(connectionInfo.username)
|
||||
}
|
||||
if ('serverName' in connectionInfo) {
|
||||
subHeaderItems.push(connectionInfo.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}
|
||||
<img src={profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
|
||||
{#if 'profilePicture' in connectionInfo}
|
||||
<img src={connectionInfo.profilePicture} alt="" class="absolute bottom-0 right-0 aspect-square h-5 rounded-full" />
|
||||
{/if}
|
||||
</div>
|
||||
<div>
|
||||
<div>{serviceData.displayName}</div>
|
||||
<div>{serviceData.displayName} - {connectionInfo.id}</div>
|
||||
<div class="text-sm text-neutral-500">
|
||||
{subHeaderItems.join(' - ')}
|
||||
</div>
|
||||
@@ -40,7 +46,7 @@
|
||||
<i class="fa-solid fa-link-slash mr-1" />
|
||||
Delete Connection
|
||||
</button>
|
||||
<input type="hidden" value={id} name="connectionId" />
|
||||
<input type="hidden" value={connectionInfo.id} name="connectionId" />
|
||||
</form>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
39
src/routes/api/audio/+server.ts
Normal file
39
src/routes/api/audio/+server.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
import ytdl from 'ytdl-core'
|
||||
|
||||
export const GET: RequestHandler = async ({ url, request }) => {
|
||||
const connectionId = url.searchParams.get('connectionId')
|
||||
const id = url.searchParams.get('id')
|
||||
if (!(connectionId && id)) return new Response('Missing query parameter', { status: 400 })
|
||||
const range = request.headers.get('range')
|
||||
if (!range) return new Response('Missing Range Header')
|
||||
|
||||
const videourl = `http://www.youtube.com/watch?v=${id}`
|
||||
|
||||
const videoInfo = await ytdl.getInfo(videourl)
|
||||
const format = ytdl.chooseFormat(videoInfo.formats, { quality: 'highestaudio', filter: 'audioonly' })
|
||||
|
||||
const audioSize = format.contentLength
|
||||
const CHUNK_SIZE = 5 * 10 ** 6
|
||||
const start = Number(range.replace(/\D/g, ''))
|
||||
const end = Math.min(start + CHUNK_SIZE, Number(audioSize) - 1)
|
||||
const contentLength = end - start + 1
|
||||
|
||||
const headers = new Headers({
|
||||
'Content-Range': `bytes ${start}-${end}/${audioSize}`,
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': contentLength.toString(),
|
||||
'Content-Type': 'audio/webm',
|
||||
})
|
||||
|
||||
const partialStream = ytdl(videourl, { format, range: { start, end } })
|
||||
|
||||
// @ts-ignore IDK enough about streaming to understand what the problem is here
|
||||
// but it appears that ytdl has a custom version of a readable stream type they use internally
|
||||
// and is what gets returned by ytdl(). Svelte will only allow you to send back the type ReadableStream
|
||||
// so it ts gets mad if you try to send back their internal type.
|
||||
// IDK to me a custom readable type seems incredibly stupid but what do I know?
|
||||
// Currently haven't found a way to convert their readable to ReadableStream type, casting doesn't seem to work either.
|
||||
return new Response(partialStream, { status: 206, headers })
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections, type ConnectionInfo } from '$lib/server/connections'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const ids = url.searchParams.get('ids')?.replace(/\s/g, '').split(',')
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections, type ConnectionInfo } from '$lib/server/connections'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
@@ -1,57 +0,0 @@
|
||||
<script lang="ts">
|
||||
import IconButton from '$lib/components/util/iconButton.svelte'
|
||||
import { goto } from '$app/navigation';
|
||||
import type { LayoutServerData } from '../$types'
|
||||
|
||||
export let data: LayoutServerData
|
||||
|
||||
interface SettingRoute {
|
||||
pathname: string
|
||||
displayName: string
|
||||
icon: string
|
||||
}
|
||||
|
||||
const accountRoutes: SettingRoute[] = [
|
||||
{
|
||||
pathname: '/settings/connections',
|
||||
displayName: 'Connections',
|
||||
icon: 'fa-solid fa-circle-nodes',
|
||||
},
|
||||
{
|
||||
pathname: '/settings/devices',
|
||||
displayName: 'Devices',
|
||||
icon: 'fa-solid fa-mobile-screen',
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<main class="grid h-full grid-rows-[min-content_auto] pb-12">
|
||||
<h1 class="sticky top-0 grid grid-cols-[1fr_auto_1fr] grid-rows-1 items-center p-6 text-2xl">
|
||||
<span class="h-12">
|
||||
<IconButton on:click={() => goto('/user')}>
|
||||
<i slot="icon" class="fa-solid fa-arrow-left" />
|
||||
</IconButton>
|
||||
</span>
|
||||
<span>Settings</span>
|
||||
</h1>
|
||||
<section class="grid grid-cols-[min-content_auto] grid-rows-1 gap-8 px-[5vw]">
|
||||
<nav class="h-full">
|
||||
<a class="whitespace-nowrap text-lg {data.url.pathname === '/settings' ? 'text-lazuli-primary' : 'text-neutral-400 hover:text-lazuli-primary'}" href="/settings">
|
||||
<i class="fa-solid fa-user mr-1 w-4 text-center" />
|
||||
Account
|
||||
</a>
|
||||
<ol class="ml-2 mt-4 flex flex-col gap-3 border-2 border-transparent border-l-neutral-500 px-2">
|
||||
{#each accountRoutes as route}
|
||||
{@const isActive = route.pathname === data.url.pathname}
|
||||
<li class="w-60 px-3 py-1">
|
||||
<a class="whitespace-nowrap {isActive ? 'text-lazuli-primary' : 'text-neutral-400 hover:text-lazuli-primary'}" href={route.pathname}>
|
||||
<i class="{route.icon} mr-1 w-4 text-center" />
|
||||
{route.displayName}
|
||||
</a>
|
||||
</li>
|
||||
{/each}
|
||||
</ol>
|
||||
</nav>
|
||||
<slot />
|
||||
</section>
|
||||
</main>
|
||||
@@ -1 +0,0 @@
|
||||
<h1>Main Settings Page</h1>
|
||||
Reference in New Issue
Block a user