How did I ever live without interfaces

This commit is contained in:
Eclypsed
2024-02-03 02:47:23 -05:00
parent 20454e22d1
commit cbe9b60973
10 changed files with 178 additions and 76 deletions

128
src/app.d.ts vendored
View File

@@ -17,10 +17,8 @@ declare global {
password?: string password?: string
} }
type ServiceType = 'jellyfin' | 'youtube-music'
interface Service { interface Service {
type: ServiceType type: 'jellyfin' | 'youtube-music'
userId: string userId: string
urlOrigin: string urlOrigin: string
} }
@@ -32,6 +30,39 @@ declare global {
accessToken: string accessToken: string
} }
interface MediaItem {
connection: Connection
id: string
name: string
duration: number
thumbnail?: string
}
interface Song extends MediaItem {
artists?: Artist[]
albumId?: string
audio: string
video?: string
releaseDate: string
}
interface Album extends MediaItem {
artists: Artist[]
songs: Song[]
releaseDate: string
}
interface Playlist extends MediaItem {
songs: Song[]
description?: string
}
interface Artist {
id: string
name: string
// Add more here in the future
}
namespace Jellyfin { namespace Jellyfin {
// The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will // The jellyfin API will not always return the data it says it will, for example /Users/AuthenticateByName says it will
// retrun the ServerName, it wont. This must be fetched from /System/Info. // retrun the ServerName, it wont. This must be fetched from /System/Info.
@@ -61,6 +92,50 @@ declare global {
interface System { interface System {
ServerName: string ServerName: string
} }
interface MediaItem {
Name: string
Id: string
RunTimeTicks: number
Type: 'Audio' | 'MusicAlbum' | 'Playlist'
ImageTags?: {
Primary?: string
}
}
interface Song extends Jellyfin.MediaItem {
ProductionYear: number
Type: 'Audio'
ArtistItems?: {
Name: string
Id: string
}[]
Album?: string
AlbumId?: string
AlbumPrimaryImageTag?: string
AlbumArtists?: {
Name: string
Id: string
}[]
}
interface Album extends Jellyfin.MediaItem {
ProductionYear: number
Type: 'MusicAlbum'
ArtistItems?: {
Name: string
Id: string
}[]
AlbumArtists?: {
Name: string
Id: string
}[]
}
interface Playlist extends Jellyfin.MediaItem {
Type: 'Playlist'
ChildCount: number
}
} }
namespace YouTubeMusic { namespace YouTubeMusic {
@@ -73,53 +148,6 @@ declare global {
service: YTService service: YTService
} }
} }
interface MediaItem {
connectionId: string
serviceType: string
id: string
name: string
duration: number
thumbnail: string
}
interface Song extends MediaItem {
artists: {
id: string
name: string
}[]
album?: {
id: string
name: string
artists: {
id: string
name: string
}[]
}
audio: string
video?: string
releaseDate: string
}
interface Album extends MediaItem {
artists: {
id: string
name: string
}[]
songs: Song[]
releaseDate: string
}
interface Playlist extends MediaItem {
songs: Song[]
description?: string
}
interface Artist {
id: string
name: string
// Add more here in the future
}
} }
export {} export {}

View File

@@ -10,11 +10,8 @@
export let disabled = false export let disabled = false
export let nav: NavTab export let nav: NavTab
import { createEventDispatcher } from 'svelte'
import { goto } from '$app/navigation' import { goto } from '$app/navigation'
const dispatch = createEventDispatcher()
let button: HTMLButtonElement let button: HTMLButtonElement
</script> </script>

View File

@@ -33,7 +33,7 @@
<button <button
{disabled} {disabled}
bind:this={button} bind:this={button}
class="relative aspect-square w-full rounded-lg bg-cover bg-center transition-all" class="relative aspect-square w-full flex-shrink-0 rounded-lg bg-cover bg-center transition-all"
style="background-image: url({playlist.thumbnail});" style="background-image: url({playlist.thumbnail});"
on:mouseenter={() => dispatch('mouseenter', { ...calculateCenter(button), content: playlist.name })} on:mouseenter={() => dispatch('mouseenter', { ...calculateCenter(button), content: playlist.name })}
on:mouseleave={() => dispatch('mouseleave')} on:mouseleave={() => dispatch('mouseleave')}

Binary file not shown.

View File

@@ -0,0 +1,42 @@
export class Jellyfin {
static audioPresets = (userId: string) => {
return {
MaxStreamingBitrate: '999999999',
Container: 'opus,webm|opus,mp3,aac,m4a|aac,m4b|aac,flac,webma,webm|webma,wav,ogg',
TranscodingContainer: 'ts',
TranscodingProtocol: 'hls',
AudioCodec: 'aac',
userId,
}
}
static mediaItemFactory = (item: Jellyfin.MediaItem, connection: Connection): MediaItem => {}
static songFactory = (song: Jellyfin.Song, connection: Connection): Song => {
const { id, service } = connection
const artists: Artist[] | undefined = song.ArtistItems
? Array.from(song.ArtistItems, (artist) => {
return { name: artist.Name, id: artist.Id }
})
: undefined
const thumbnail = song.ImageTags?.Primary ? new URL(`Items/${song.Id}/Images/Primary`, service.urlOrigin).href : song.AlbumPrimaryImageTag ? new URL(`Items/${song.AlbumId}/Images/Primary`).href : undefined
const audoSearchParams = new URLSearchParams(this.audioPresets(service.userId))
const audioSource = new URL(`Audio/${song.Id}/universal?${audoSearchParams.toString()}`, service.urlOrigin).href
const factorySong: Song = {
connection,
id: song.Id,
name: song.Name,
duration: Math.floor(song.RunTimeTicks / 10000),
thumbnail,
artists,
albumId: song.AlbumId,
audio: audioSource,
releaseDate: String(song.ProductionYear),
}
return factorySong
}
}

View File

@@ -7,7 +7,7 @@ export const GET: RequestHandler = async ({ params }) => {
const userId = params.userId as string const userId = params.userId as string
const connections = Connections.getUserConnections(userId) const connections = Connections.getUserConnections(userId)
return new Response(JSON.stringify(connections)) return Response.json(connections)
} }
// This schema should be identical to the Connection Data Type but without the id and userId // This schema should be identical to the Connection Data Type but without the id and userId
@@ -30,7 +30,7 @@ export const POST: RequestHandler = async ({ params, request }) => {
const { service, accessToken } = connection const { service, accessToken } = connection
const newConnection = Connections.addConnection(userId, service, accessToken) const newConnection = Connections.addConnection(userId, service, accessToken)
return new Response(JSON.stringify(newConnection)) return Response.json(newConnection)
} }
export const DELETE: RequestHandler = async ({ request }) => { export const DELETE: RequestHandler = async ({ request }) => {

View File

@@ -0,0 +1,34 @@
import type { RequestHandler } from '@sveltejs/kit'
import { SECRET_INTERNAL_API_KEY } from '$env/static/private'
// This is temporary functionally for the sake of developing the app.
// In the future will implement more robust algorith for offering recommendations
export const GET: RequestHandler = async ({ params, fetch }) => {
const userId = params.userId as string
const connectionsResponse = await fetch(`/api/users/${userId}/connections`, { headers: { apikey: SECRET_INTERNAL_API_KEY } })
const userConnections: Connection[] = await connectionsResponse.json()
const recommendations = []
for (const connection of userConnections) {
const { service, accessToken } = connection
switch (service.type) {
case 'jellyfin':
const mostPlayedSongsSearchParams = new URLSearchParams({
SortBy: 'PlayCount',
SortOrder: 'Descending',
IncludeItemTypes: 'Audio',
Recursive: 'true',
limit: '10',
})
const mostPlayedSongsURL = new URL(`/Users/${service.userId}/Items?${mostPlayedSongsSearchParams.toString()}`, service.urlOrigin).href
const requestHeaders = new Headers({ Authorization: `MediaBrowser Token="${accessToken}"` })
const mostPlayedResponse = await fetch(mostPlayedSongsURL, { headers: requestHeaders })
const mostPlayedData = await mostPlayedResponse.json()
}
}
}

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import { fly } from 'svelte/transition'
import Services from '$lib/services.json' import Services from '$lib/services.json'
import JellyfinAuthBox from './jellyfinAuthBox.svelte' import JellyfinAuthBox from './jellyfinAuthBox.svelte'
import { newestAlert } from '$lib/stores.js' import { newestAlert } from '$lib/stores.js'
@@ -38,26 +37,25 @@
} }
return async ({ result }) => { return async ({ result }) => {
switch (result.type) { if (result.type === 'failure') {
case 'failure': return ($newestAlert = ['warning', result.data?.message])
return ($newestAlert = ['warning', result.data?.message]) } else if (result.type === 'success') {
case 'success': if (result.data?.newConnection) {
if (result.data?.newConnection) { const newConnection: Connection = result.data.newConnection
const newConnection: Connection = result.data.newConnection connections = [newConnection, ...connections]
connections = [newConnection, ...connections]
newConnectionModal = null newConnectionModal = null
return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`]) return ($newestAlert = ['success', `Added ${Services[newConnection.service.type].displayName}`])
} else if (result.data?.deletedConnectionId) { } else if (result.data?.deletedConnectionId) {
const id = result.data.deletedConnectionId const id = result.data.deletedConnectionId
const indexToDelete = connections.findIndex((connection) => connection.id === id) const indexToDelete = connections.findIndex((connection) => connection.id === id)
const serviceType = connections[indexToDelete].service.type const serviceType = connections[indexToDelete].service.type
connections.splice(indexToDelete, 1) connections.splice(indexToDelete, 1)
connections = connections connections = connections
return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`]) return ($newestAlert = ['success', `Deleted ${Services[serviceType].displayName}`])
} }
} }
} }
} }
@@ -82,7 +80,9 @@
<ConnectionProfile {connection} submitFunction={submitCredentials} /> <ConnectionProfile {connection} submitFunction={submitCredentials} />
{/each} {/each}
</div> </div>
<svelte:component this={newConnectionModal} submitFunction={submitCredentials} on:close={() => (newConnectionModal = null)} /> {#if newConnectionModal !== null}
<svelte:component this={newConnectionModal} submitFunction={submitCredentials} on:close={() => (newConnectionModal = null)} />
{/if}
</main> </main>
<style> <style>

View File

@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { SubmitFunction } from '@sveltejs/kit' import type { SubmitFunction } from '@sveltejs/kit'
import { scale } from 'svelte/transition'
import { createEventDispatcher } from 'svelte' import { createEventDispatcher } from 'svelte'
import { enhance } from '$app/forms' import { enhance } from '$app/forms'
@@ -8,7 +9,7 @@
const dispatch = createEventDispatcher() const dispatch = createEventDispatcher()
</script> </script>
<form method="post" use:enhance={submitFunction} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2"> <form method="post" use:enhance={submitFunction} class="fixed left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" transition:scale>
<div id="main-box" class="relative flex aspect-video w-screen max-w-2xl flex-col justify-center gap-9 rounded-xl bg-neutral-925 px-8"> <div id="main-box" class="relative flex aspect-video w-screen max-w-2xl flex-col justify-center gap-9 rounded-xl bg-neutral-925 px-8">
<h1 class="text-center text-4xl">Jellyfin Sign In</h1> <h1 class="text-center text-4xl">Jellyfin Sign In</h1>
<div class="flex w-full flex-col gap-5"> <div class="flex w-full flex-col gap-5">