How did I ever live without interfaces
This commit is contained in:
128
src/app.d.ts
vendored
128
src/app.d.ts
vendored
@@ -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 {}
|
||||||
|
|||||||
0
src/lib/components/media/mediaCard.svelte
Normal file
0
src/lib/components/media/mediaCard.svelte
Normal 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>
|
||||||
|
|
||||||
|
|||||||
@@ -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.
42
src/lib/service-managers/jellyfin.ts
Normal file
42
src/lib/service-managers/jellyfin.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }) => {
|
||||||
|
|||||||
34
src/routes/api/users/[userId]/recommendations/+server.ts
Normal file
34
src/routes/api/users/[userId]/recommendations/+server.ts
Normal 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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,10 +37,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
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])
|
||||||
case 'success':
|
} else if (result.type === '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]
|
||||||
@@ -82,7 +80,9 @@
|
|||||||
<ConnectionProfile {connection} submitFunction={submitCredentials} />
|
<ConnectionProfile {connection} submitFunction={submitCredentials} />
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
{#if newConnectionModal !== null}
|
||||||
<svelte:component this={newConnectionModal} submitFunction={submitCredentials} on:close={() => (newConnectionModal = null)} />
|
<svelte:component this={newConnectionModal} submitFunction={submitCredentials} on:close={() => (newConnectionModal = null)} />
|
||||||
|
{/if}
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
Reference in New Issue
Block a user