Dropped ytdl, YT audio now fetched with Android Client. Began work on YT Premium support
This commit is contained in:
23
src/routes/(app)/details/album/+page.server.ts
Normal file
23
src/routes/(app)/details/album/+page.server.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
|
||||
import type { PageServerLoad } from './$types'
|
||||
|
||||
export const load: PageServerLoad = async ({ fetch, url }) => {
|
||||
const connectionId = url.searchParams.get('connection')
|
||||
const id = url.searchParams.get('id')
|
||||
|
||||
async function getAlbum(): Promise<Album> {
|
||||
const albumResponse = (await fetch(`/api/connections/${connectionId}/album?id=${id}`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())) as { album: Album }
|
||||
return albumResponse.album
|
||||
}
|
||||
|
||||
async function getAlbumItems(): Promise<Song[]> {
|
||||
const itemsResponse = (await fetch(`/api/connections/${connectionId}/album/${id}/items`, {
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => response.json())) as { items: Song[] }
|
||||
return itemsResponse.items
|
||||
}
|
||||
|
||||
return { albumDetails: Promise.all([getAlbum(), getAlbumItems()]) }
|
||||
}
|
||||
34
src/routes/(app)/details/album/+page.svelte
Normal file
34
src/routes/(app)/details/album/+page.svelte
Normal file
@@ -0,0 +1,34 @@
|
||||
<script lang="ts">
|
||||
import Loader from '$lib/components/util/loader.svelte'
|
||||
import type { PageData } from './$types'
|
||||
|
||||
export let data: PageData
|
||||
</script>
|
||||
|
||||
<main>
|
||||
{#await data.albumDetails}
|
||||
<Loader />
|
||||
{:then [album, items]}
|
||||
<section class="flex gap-8">
|
||||
<img class="h-60" src="/api/remoteImage?url={album.thumbnailUrl}" alt="{album.name} cover art" />
|
||||
<div>
|
||||
<div class="text-4xl">{album.name}</div>
|
||||
{#if album.artists === 'Various Artists'}
|
||||
<div>Various Artists</div>
|
||||
{:else}
|
||||
<div style="font-size: 0;">
|
||||
{#each album.artists as artist, index}
|
||||
<a class="text-sm hover:underline focus:underline" href="/details/artist?id={artist.id}&connection={album.connection.id}">{artist.name}</a>
|
||||
{#if index < album.artists.length - 1}
|
||||
<span class="mr-0.5 text-sm">,</span>
|
||||
{/if}
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
{#each items as item}
|
||||
<div>{item.name}</div>
|
||||
{/each}
|
||||
{/await}
|
||||
</main>
|
||||
@@ -1,4 +1,5 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation'
|
||||
import { queue } from '$lib/stores'
|
||||
import type { PageServerData } from './$types'
|
||||
|
||||
@@ -24,7 +25,11 @@
|
||||
<button
|
||||
id="searchResult"
|
||||
on:click={() => {
|
||||
if (searchResult.type === 'song') queueRef.current = searchResult
|
||||
if (searchResult.type === 'song') {
|
||||
queueRef.current = searchResult
|
||||
} else {
|
||||
goto(`/details/${searchResult.type}?id=${searchResult.id}&connection=${searchResult.connection.id}`)
|
||||
}
|
||||
}}
|
||||
class="grid aspect-square h-full place-items-center bg-cover bg-center bg-no-repeat"
|
||||
style="--thumbnail: url('/api/remoteImage?url={'thumbnailUrl' in searchResult ? searchResult.thumbnailUrl : searchResult.profilePicture}')"
|
||||
|
||||
@@ -31,7 +31,7 @@ export const actions: Actions = {
|
||||
|
||||
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}`, {
|
||||
const response = await fetch(`/api/connections?id=${newConnectionId}`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
}).then((response) => {
|
||||
@@ -57,7 +57,7 @@ export const actions: Actions = {
|
||||
tokens: { accessToken: tokens.access_token!, refreshToken: tokens.refresh_token!, expiry: tokens.expiry_date! },
|
||||
})
|
||||
|
||||
const response = await fetch(`/api/connections?ids=${newConnectionId}`, {
|
||||
const response = await fetch(`/api/connections?id=${newConnectionId}`, {
|
||||
method: 'GET',
|
||||
headers: { apikey: SECRET_INTERNAL_API_KEY },
|
||||
})
|
||||
|
||||
@@ -5,26 +5,20 @@ export const GET: RequestHandler = async ({ url, request }) => {
|
||||
const connectionId = url.searchParams.get('connection')
|
||||
const id = url.searchParams.get('id')
|
||||
if (!(connectionId && id)) return new Response('Missing query parameter', { status: 400 })
|
||||
// Might want to re-evaluate how specific I make these ^ v error response messages
|
||||
const connection = Connections.getConnection(connectionId)
|
||||
if (!connection) return new Response('Invalid connection id', { status: 400 })
|
||||
|
||||
const range = request.headers.get('range')
|
||||
const connection = Connections.getConnections([connectionId])[0]
|
||||
const audioRequestHeaders = new Headers({ range: request.headers.get('range') ?? 'bytes=0-' })
|
||||
|
||||
const fetchStream = async (): Promise<Response> => {
|
||||
const MAX_TRIES = 5
|
||||
let tries = 0
|
||||
while (tries < MAX_TRIES) {
|
||||
++tries
|
||||
const stream = await connection.getAudioStream(id, range).catch((reason) => {
|
||||
console.error(`Audio stream fetch failed: ${reason}`)
|
||||
return null
|
||||
})
|
||||
if (!stream || !stream.ok) continue
|
||||
const response = await connection
|
||||
.getAudioStream(id, audioRequestHeaders)
|
||||
// * Withing the .getAudioStream() method of connections, a TypeError should be thrown if the request was invalid (e.g. non-existent id)
|
||||
// * A standard Error should be thrown if the fetch to the service's server failed or the request returned invalid data
|
||||
.catch((error: TypeError | Error) => {
|
||||
if (error instanceof TypeError) return new Response('Malformed Request', { status: 400 })
|
||||
return new Response('Failed to fetch valid audio stream', { status: 502 })
|
||||
})
|
||||
|
||||
return stream
|
||||
}
|
||||
|
||||
throw new Error(`Audio stream fetch to connection: ${connection.id} of id ${id} failed`)
|
||||
}
|
||||
|
||||
return await fetchStream()
|
||||
return response
|
||||
}
|
||||
|
||||
@@ -2,16 +2,21 @@ import type { RequestHandler } from '@sveltejs/kit'
|
||||
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 ids = url.searchParams.get('id')?.replace(/\s/g, '').split(',')
|
||||
if (!ids) return new Response('Missing id query parameter', { status: 400 })
|
||||
|
||||
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}`))
|
||||
}
|
||||
const connections = (
|
||||
await Promise.all(
|
||||
ids.map((id) =>
|
||||
Connections.getConnection(id)
|
||||
?.getConnectionInfo()
|
||||
.catch((reason) => {
|
||||
console.error(`Failed to fetch connection info: ${reason}`)
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
).filter((connection): connection is ConnectionInfo => connection?.id !== undefined)
|
||||
|
||||
return Response.json({ connections })
|
||||
}
|
||||
|
||||
16
src/routes/api/connections/[connectionId]/album/+server.ts
Normal file
16
src/routes/api/connections/[connectionId]/album/+server.ts
Normal file
@@ -0,0 +1,16 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const connectionId = params.connectionId!
|
||||
const connection = Connections.getConnection(connectionId)
|
||||
if (!connection) return new Response('Invalid connection id', { status: 400 })
|
||||
|
||||
const albumId = url.searchParams.get('id')
|
||||
if (!albumId) return new Response(`Missing id search parameter`, { status: 400 })
|
||||
|
||||
const album = await connection.getAlbum(albumId).catch(() => undefined)
|
||||
if (!album) return new Response(`Failed to fetch album with id: ${albumId}`, { status: 400 })
|
||||
|
||||
return Response.json({ album })
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const { connectionId, albumId } = params
|
||||
const connection = Connections.getConnection(connectionId!)
|
||||
if (!connection) return new Response('Invalid connection id', { status: 400 })
|
||||
|
||||
const items = await connection.getAlbumItems(albumId!).catch(() => undefined)
|
||||
if (!items) return new Response(`Failed to fetch album with id: ${albumId!}`, { status: 400 })
|
||||
|
||||
return Response.json({ items })
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params, url }) => {
|
||||
const connectionId = params.connectionId!
|
||||
const connection = Connections.getConnection(connectionId)
|
||||
if (!connection) return new Response('Invalid connection id', { status: 400 })
|
||||
|
||||
const playlistId = url.searchParams.get('id')
|
||||
if (!playlistId) return new Response(`Missing id search parameter`, { status: 400 })
|
||||
|
||||
const playlist = await connection.getPlaylist(playlistId).catch(() => undefined)
|
||||
if (!playlist) return new Response(`Failed to fetch playlist with id: ${playlistId}`, { status: 400 })
|
||||
|
||||
return Response.json({ playlist })
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const { connectionId, playlistId } = params
|
||||
const connection = Connections.getConnection(connectionId!)
|
||||
if (!connection) return new Response('Invalid connection id', { status: 400 })
|
||||
|
||||
const items = await connection.getPlaylistItems(playlistId!).catch((reason) => console.error(reason))
|
||||
if (!items) return new Response(`Failed to fetch playlist with id: ${playlistId!}`, { status: 400 })
|
||||
|
||||
return Response.json({ items })
|
||||
}
|
||||
@@ -11,19 +11,16 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
let tries = 0
|
||||
while (tries < MAX_TRIES) {
|
||||
++tries
|
||||
const response = await fetch(imageUrl).catch((reason) => {
|
||||
console.error(`Image fetch to ${imageUrl} failed: ${reason}`)
|
||||
return null
|
||||
})
|
||||
const response = await fetch(imageUrl).catch(() => null)
|
||||
if (!response || !response.ok) continue
|
||||
|
||||
const contentType = response.headers.get('content-type')
|
||||
if (!contentType || !contentType.startsWith('image')) throw new Error(`Url ${imageUrl} does not link to an image`)
|
||||
if (!contentType || !contentType.startsWith('image')) throw Error(`Url ${imageUrl} does not link to an image`)
|
||||
|
||||
return response
|
||||
}
|
||||
|
||||
throw new Error('Exceed Max Retires')
|
||||
throw Error(`Failed to fetch image at ${url} Exceed Max Retires`)
|
||||
}
|
||||
|
||||
return await fetchImage()
|
||||
|
||||
@@ -2,18 +2,27 @@ import type { RequestHandler } from '@sveltejs/kit'
|
||||
import { Connections } from '$lib/server/connections'
|
||||
|
||||
export const GET: RequestHandler = async ({ url }) => {
|
||||
const query = url.searchParams.get('query')
|
||||
if (!query) return new Response('Missing query parameter', { status: 400 })
|
||||
const userId = url.searchParams.get('userId')
|
||||
if (!userId) return new Response('Missing userId parameter', { status: 400 })
|
||||
const { query, userId, filter } = Object.fromEntries(url.searchParams) as { [k: string]: string | undefined }
|
||||
if (!(query && userId)) return new Response('Missing search parameter', { status: 400 })
|
||||
|
||||
const searchResults: (Song | Album | Artist | Playlist)[] = []
|
||||
for (const connection of Connections.getUserConnections(userId)) {
|
||||
await connection
|
||||
.search(query)
|
||||
.then((results) => searchResults.push(...results))
|
||||
.catch((reason) => console.error(`Failed to search "${query}" from connection ${connection.id}: ${reason}`))
|
||||
}
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
let checkedFilter: 'song' | 'album' | 'artist' | 'playlist' | undefined
|
||||
if (filter === 'song' || filter === 'album' || filter === 'artist' || filter === 'playlist') checkedFilter = filter
|
||||
|
||||
const searchResults = (
|
||||
await Promise.all(
|
||||
userConnections.map((connection) =>
|
||||
connection.search(query, checkedFilter).catch((reason) => {
|
||||
console.error(`Failed to search "${query}" from connection ${connection.id}: ${reason}`)
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((result): result is Song | Album | Artist | Playlist => result?.id !== undefined)
|
||||
|
||||
return Response.json({ searchResults })
|
||||
}
|
||||
|
||||
@@ -4,13 +4,19 @@ import { Connections } from '$lib/server/connections'
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
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}`))
|
||||
}
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
const connections = (
|
||||
await Promise.all(
|
||||
userConnections.map((connection) =>
|
||||
connection.getConnectionInfo().catch((reason) => {
|
||||
console.log(`Failed to fetch connection info: ${reason}`)
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
).filter((info): info is ConnectionInfo => info !== undefined)
|
||||
|
||||
return Response.json({ connections })
|
||||
}
|
||||
|
||||
@@ -6,13 +6,21 @@ import { Connections } from '$lib/server/connections'
|
||||
export const GET: RequestHandler = async ({ params }) => {
|
||||
const userId = params.userId!
|
||||
|
||||
const recommendations: (Song | Album | Artist | Playlist)[] = []
|
||||
for (const connection of Connections.getUserConnections(userId)) {
|
||||
await connection
|
||||
.getRecommendations()
|
||||
.then((connectionRecommendations) => recommendations.push(...connectionRecommendations))
|
||||
.catch((reason) => console.log(`Failed to fetch recommendations: ${reason}`))
|
||||
}
|
||||
const userConnections = Connections.getUserConnections(userId)
|
||||
if (!userConnections) return new Response('Invalid user id', { status: 400 })
|
||||
|
||||
const recommendations = (
|
||||
await Promise.all(
|
||||
userConnections.map((connection) =>
|
||||
connection.getRecommendations().catch((reason) => {
|
||||
console.log(`Failed to fetch recommendations: ${reason}`)
|
||||
return undefined
|
||||
}),
|
||||
),
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((recommendation): recommendation is Song | Album | Artist | Playlist => recommendation?.id !== undefined)
|
||||
|
||||
return Response.json({ recommendations })
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user