Updated Jellyfin media item parsers, added startIndex && limit params to YTMusic getPlaylistItems() method

This commit is contained in:
Eclypsed
2024-05-29 01:25:17 -04:00
parent 11497f8b91
commit a98b258a03
10 changed files with 303 additions and 230 deletions

View File

@@ -0,0 +1,24 @@
<script lang="ts">
import Loader from '$lib/components/util/loader.svelte'
import type { PageData } from './$types'
export let data: PageData
</script>
<main>
{#await data.playlistDetails}
<Loader />
{:then [playlist, items]}
<section class="flex gap-8">
<img class="h-60" src="/api/remoteImage?url={playlist.thumbnailUrl}" alt="{playlist.name} cover art" />
<div>
<div class="text-4xl">{playlist.name}</div>
</div>
</section>
{#each items as item}
<div>{item.name}</div>
{/each}
{:catch}
<div>Failed to fetch playlist</div>
{/await}
</main>

View File

@@ -0,0 +1,22 @@
import type { PageLoad } from './$types'
export const load: PageLoad = async ({ fetch, url }) => {
const connectionId = url.searchParams.get('connection')
const id = url.searchParams.get('id')
async function getPlaylist() {
const playlistResponse = (await fetch(`/api/connections/${connectionId}/playlist?id=${id}`, {
credentials: 'include',
}).then((response) => response.json())) as { playlist: Playlist }
return playlistResponse.playlist
}
async function getPlaylistItems() {
const itemsResponse = (await fetch(`/api/connections/${connectionId}/playlist/${id}/items`, {
credentials: 'include',
}).then((response) => response.json())) as { items: Song[] }
return itemsResponse.items
}
return { playlistDetails: Promise.all([getPlaylist(), getPlaylistItems()]) }
}

View File

@@ -1,66 +1,16 @@
<script lang="ts">
import { goto } from '$app/navigation'
import { queue } from '$lib/stores'
import type { PageServerData } from './$types'
let queueRef = $queue // This nonsense is to prevent an bug that causes svelte to throw an error when setting a property of the queue directly
import MediaCard from '$lib/components/media/mediaCard.svelte'
export let data: PageServerData
const formatTime = (seconds: number): string => {
seconds = Math.floor(seconds)
const hours = Math.floor(seconds / 3600)
seconds = seconds - hours * 3600
const minutes = Math.floor(seconds / 60)
seconds = seconds - minutes * 60
return hours > 0 ? `${hours}:${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}` : `${minutes}:${seconds.toString().padStart(2, '0')}`
}
</script>
{#if data.searchResults}
{#await data.searchResults then searchResults}
<section class="flex w-full flex-col items-center gap-2">
<section class="flex w-full flex-wrap gap-6">
{#each searchResults as searchResult}
<div class="flex h-20 w-full max-w-screen-md gap-4 bg-black p-2">
<button
id="searchResult"
on:click={() => {
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}')"
>
<i class="fa-solid fa-play opacity-0" />
</button>
<div>
<div>{searchResult.name}{searchResult.type === 'song' && searchResult.album?.name ? ` - ${searchResult.album.name}` : ''}</div>
{#if 'artists' in searchResult && searchResult.artists}
<div>{searchResult.artists === 'Various Artists' ? searchResult.artists : searchResult.artists.map((artist) => artist.name).join(', ')}</div>
{:else if 'createdBy' in searchResult && searchResult.createdBy}
<div>{searchResult.createdBy?.name}</div>
{/if}
</div>
{#if 'duration' in searchResult && searchResult.duration}
<span class="justify-self-end">{formatTime(searchResult.duration)}</span>
{/if}
</div>
<MediaCard mediaItem={searchResult} />
{/each}
</section>
{/await}
{/if}
<style>
#searchResult {
background-image: var(--thumbnail);
}
#searchResult:hover {
background-image: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0.5)), var(--thumbnail);
}
#searchResult:hover > i {
opacity: 100%;
}
</style>

View File

@@ -16,7 +16,7 @@ export const GET: RequestHandler = async ({ url, request }) => {
// * 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 })
if (error instanceof TypeError) return new Response('Bad Request', { status: 400 })
return new Response('Failed to fetch valid audio stream', { status: 502 })
})

View File

@@ -9,8 +9,13 @@ export const GET: RequestHandler = async ({ params, url }) => {
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 })
const response = await connection
.getPlaylistItems(playlistId)
.then((playlist) => Response.json({ playlist }))
.catch((error: TypeError | Error) => {
if (error instanceof TypeError) return new Response('Bad Request', { status: 400 })
return new Response('Failed to fetch playlist items', { status: 502 })
})
return Response.json({ playlist })
return response
}

View File

@@ -1,13 +1,27 @@
import type { RequestHandler } from '@sveltejs/kit'
import { Connections } from '$lib/server/connections'
export const GET: RequestHandler = async ({ params }) => {
export const GET: RequestHandler = async ({ params, url }) => {
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 })
const startIndexString = url.searchParams.get('startIndex')
const limitString = url.searchParams.get('limit')
return Response.json({ items })
const numberStartIndex = Number(startIndexString)
const numberLimit = Number(limitString)
const startIndex = Number.isInteger(numberStartIndex) && numberStartIndex > 0 ? numberStartIndex : undefined
const limit = Number.isInteger(numberLimit) && numberLimit > 0 ? numberLimit : undefined
const response = await connection
.getPlaylistItems(playlistId!, startIndex, limit)
.then((items) => Response.json({ items }))
.catch((error: TypeError | Error) => {
if (error instanceof TypeError) return new Response('Bad Request', { status: 400 })
return new Response('Failed to fetch playlist items', { status: 502 })
})
return response
}