diff --git a/src/lib/apis/jellyfin/jellyfinApi.ts b/src/lib/apis/jellyfin/jellyfinApi.ts index c3e7bbf..1276487 100644 --- a/src/lib/apis/jellyfin/jellyfinApi.ts +++ b/src/lib/apis/jellyfin/jellyfinApi.ts @@ -1,27 +1,29 @@ -import createClient from 'openapi-fetch'; -import type { components, paths } from '$lib/apis/jellyfin/jellyfin.generated'; import { env } from '$env/dynamic/public'; -import { request } from '$lib/utils'; +import type { components, paths } from '$lib/apis/jellyfin/jellyfin.generated'; import type { DeviceProfile } from '$lib/apis/jellyfin/playback-profiles'; -import { settings } from '$lib/stores/settings.store'; -import { get } from 'svelte/store'; +import createClient from 'openapi-fetch'; export type JellyfinItem = components['schemas']['BaseItemDto']; +export const jellyfinAvailable = !!env.PUBLIC_JELLYFIN_URL && !!env.PUBLIC_JELLYFIN_API_KEY; + export const JELLYFIN_DEVICE_ID = 'Reiverr Client'; -export const JellyfinApi = createClient({ - baseUrl: env.PUBLIC_JELLYFIN_URL, - headers: { - Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${env.PUBLIC_JELLYFIN_API_KEY}"` - } -}); +export const JellyfinApi = + env.PUBLIC_JELLYFIN_URL && env.PUBLIC_JELLYFIN_API_KEY + ? createClient({ + baseUrl: env.PUBLIC_JELLYFIN_URL, + headers: { + Authorization: `MediaBrowser DeviceId="${JELLYFIN_DEVICE_ID}", Token="${env.PUBLIC_JELLYFIN_API_KEY}"` + } + }) + : undefined; let userId: string | undefined = undefined; const getUserId = async () => { if (userId) return userId; - const user = JellyfinApi.get('/Users', { + const user = JellyfinApi?.get('/Users', { params: {}, headers: { 'cache-control': 'max-age=3600' @@ -32,43 +34,55 @@ const getUserId = async () => { return user; }; -export const getJellyfinContinueWatching = async (): Promise => - JellyfinApi.get('/Users/{userId}/Items/Resume', { - params: { - path: { - userId: await getUserId() - }, - query: { - mediaTypes: ['Video'], - fields: ['ProviderIds'] - } - } - }).then((r) => r.data?.Items || []); +export const getJellyfinContinueWatching = async (): Promise => + getUserId().then((userId) => + userId + ? JellyfinApi?.get('/Users/{userId}/Items/Resume', { + params: { + path: { + userId + }, + query: { + mediaTypes: ['Video'], + fields: ['ProviderIds'] + } + } + }).then((r) => r.data?.Items || []) + : undefined + ); export const getJellyfinNextUp = async () => - JellyfinApi.get('/Shows/NextUp', { - params: { - query: { - userId: await getUserId(), - fields: ['ProviderIds'] - } - } - }).then((r) => r.data?.Items || []); + getUserId().then((userId) => + userId + ? JellyfinApi?.get('/Shows/NextUp', { + params: { + query: { + userId, + fields: ['ProviderIds'] + } + } + }).then((r) => r.data?.Items || []) + : undefined + ); export const getJellyfinItems = async () => - JellyfinApi.get('/Users/{userId}/Items', { - params: { - path: { - userId: await getUserId() - }, - query: { - hasTmdbId: true, - recursive: true, - includeItemTypes: ['Movie', 'Series'], - fields: ['ProviderIds'] - } - } - }).then((r) => r.data?.Items || []); + getUserId().then((userId) => + userId + ? JellyfinApi?.get('/Users/{userId}/Items', { + params: { + path: { + userId + }, + query: { + hasTmdbId: true, + recursive: true, + includeItemTypes: ['Movie', 'Series'], + fields: ['ProviderIds'] + } + } + }).then((r) => r.data?.Items || []) + : undefined + ); // export const getJellyfinSeries = () => // JellyfinApi.get('/Users/{userId}/Items', { @@ -85,70 +99,82 @@ export const getJellyfinItems = async () => // }).then((r) => r.data?.Items || []); export const getJellyfinEpisodes = async () => - JellyfinApi.get('/Users/{userId}/Items', { - params: { - path: { - userId: await getUserId() - }, - query: { - recursive: true, - includeItemTypes: ['Episode'] - } - }, - headers: { - 'cache-control': 'max-age=10' - } - }).then((r) => r.data?.Items || []); + getUserId().then((userId) => + userId + ? JellyfinApi?.get('/Users/{userId}/Items', { + params: { + path: { + userId + }, + query: { + recursive: true, + includeItemTypes: ['Episode'] + } + }, + headers: { + 'cache-control': 'max-age=10' + } + }).then((r) => r.data?.Items || []) + : undefined + ); -export const getJellyfinEpisodesBySeries = (seriesId: string) => - getJellyfinEpisodes().then((items) => items.filter((i) => i.SeriesId === seriesId) || []); +// export const getJellyfinEpisodesBySeries = (seriesId: string) => +// getJellyfinEpisodes().then((items) => items?.filter((i) => i.SeriesId === seriesId) || []); -export const getJellyfinItemByTmdbId = (tmdbId: string) => - getJellyfinItems().then((items) => items.find((i) => i.ProviderIds?.Tmdb == tmdbId)); +// export const getJellyfinItemByTmdbId = (tmdbId: string) => +// getJellyfinItems().then((items) => items?.find((i) => i.ProviderIds?.Tmdb == tmdbId)); export const getJellyfinItem = async (itemId: string) => - JellyfinApi.get('/Users/{userId}/Items/{itemId}', { - params: { - path: { - itemId, - userId: await getUserId() - } - } - }).then((r) => r.data); + getUserId().then((userId) => + userId + ? JellyfinApi?.get('/Users/{userId}/Items/{itemId}', { + params: { + path: { + itemId, + userId + } + } + }).then((r) => r.data) + : undefined + ); -export const requestJellyfinItemByTmdbId = () => - request((tmdbId: string) => getJellyfinItemByTmdbId(tmdbId)); +// export const requestJellyfinItemByTmdbId = () => +// request((tmdbId: string) => getJellyfinItemByTmdbId(tmdbId)); export const getJellyfinPlaybackInfo = async ( itemId: string, playbackProfile: DeviceProfile, startTimeTicks = 0 ) => - JellyfinApi.post('/Items/{itemId}/PlaybackInfo', { - params: { - path: { - itemId: itemId - }, - query: { - userId: await getUserId(), - startTimeTicks, - autoOpenLiveStream: true, - maxStreamingBitrate: 140000000 - } - }, - body: { - DeviceProfile: playbackProfile - } - }).then((r) => ({ - playbackUri: - r.data?.MediaSources?.[0]?.TranscodingUrl || - `/Videos/${r.data?.MediaSources?.[0].Id}/stream.mp4?Static=true&mediaSourceId=${r.data?.MediaSources?.[0].Id}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${PUBLIC_JELLYFIN_API_KEY}&Tag=${r.data?.MediaSources?.[0].ETag}`, - mediaSourceId: r.data?.MediaSources?.[0]?.Id, - playSessionId: r.data?.PlaySessionId, - directPlay: - !!r.data?.MediaSources?.[0]?.SupportsDirectPlay || - !!r.data?.MediaSources?.[0]?.SupportsDirectStream - })); + getUserId().then((userId) => + userId + ? JellyfinApi?.post('/Items/{itemId}/PlaybackInfo', { + params: { + path: { + itemId: itemId + }, + query: { + userId, + startTimeTicks, + autoOpenLiveStream: true, + maxStreamingBitrate: 140000000 + } + }, + body: { + DeviceProfile: playbackProfile + } + }).then((r) => ({ + playbackUri: + r.data?.MediaSources?.[0]?.TranscodingUrl || + `/Videos/${r.data?.MediaSources?.[0].Id}/stream.mp4?Static=true&mediaSourceId=${r.data?.MediaSources?.[0].Id}&deviceId=${JELLYFIN_DEVICE_ID}&api_key=${PUBLIC_JELLYFIN_API_KEY}&Tag=${r.data?.MediaSources?.[0].ETag}`, + mediaSourceId: r.data?.MediaSources?.[0]?.Id, + playSessionId: r.data?.PlaySessionId, + directPlay: + !!r.data?.MediaSources?.[0]?.SupportsDirectPlay || + !!r.data?.MediaSources?.[0]?.SupportsDirectStream + })) + : undefined + ); export const reportJellyfinPlaybackStarted = ( itemId: string, @@ -157,16 +183,20 @@ export const reportJellyfinPlaybackStarted = ( audioStreamIndex?: number, subtitleStreamIndex?: number ) => - JellyfinApi.post('/Sessions/Playing', { - body: { - CanSeek: true, - ItemId: itemId, - PlaySessionId: sessionId, - MediaSourceId: mediaSourceId, - AudioStreamIndex: 1, - SubtitleStreamIndex: -1 - } - }); + getUserId().then((userId) => + userId + ? JellyfinApi?.post('/Sessions/Playing', { + body: { + CanSeek: true, + ItemId: itemId, + PlaySessionId: sessionId, + MediaSourceId: mediaSourceId, + AudioStreamIndex: 1, + SubtitleStreamIndex: -1 + } + }) + : undefined + ); export const reportJellyfinPlaybackProgress = ( itemId: string, @@ -174,7 +204,7 @@ export const reportJellyfinPlaybackProgress = ( isPaused: boolean, positionTicks: number ) => - JellyfinApi.post('/Sessions/Playing/Progress', { + JellyfinApi?.post('/Sessions/Playing/Progress', { body: { ItemId: itemId, PlaySessionId: sessionId, @@ -190,7 +220,7 @@ export const reportJellyfinPlaybackStopped = ( sessionId: string, positionTicks: number ) => - JellyfinApi.post('/Sessions/Playing/Stopped', { + JellyfinApi?.post('/Sessions/Playing/Stopped', { body: { ItemId: itemId, PlaySessionId: sessionId, @@ -200,24 +230,32 @@ export const reportJellyfinPlaybackStopped = ( }); export const setJellyfinItemWatched = async (jellyfinId: string) => - JellyfinApi.post('/Users/{userId}/PlayedItems/{itemId}', { - params: { - path: { - userId: await getUserId(), - itemId: jellyfinId - }, - query: { - datePlayed: new Date().toISOString() - } - } - }); + getUserId().then((userId) => + userId + ? JellyfinApi?.post('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId, + itemId: jellyfinId + }, + query: { + datePlayed: new Date().toISOString() + } + } + }) + : undefined + ); export const setJellyfinItemUnwatched = async (jellyfinId: string) => - JellyfinApi.del('/Users/{userId}/PlayedItems/{itemId}', { - params: { - path: { - userId: await getUserId(), - itemId: jellyfinId - } - } - }); + getUserId().then((userId) => + userId + ? JellyfinApi?.del('/Users/{userId}/PlayedItems/{itemId}', { + params: { + path: { + userId, + itemId: jellyfinId + } + } + }) + : undefined + ); diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index 8966c98..23c9e61 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -1,16 +1,14 @@ @@ -63,7 +59,7 @@ - window.open(PUBLIC_RADARR_BASE_URL + '/movie/' + $itemStore.item?.radarrMovie?.tmdbId)} + window.open(env.PUBLIC_RADARR_BASE_URL + '/movie/' + $itemStore.item?.radarrMovie?.tmdbId)} > Open in Radarr @@ -71,7 +67,9 @@ - window.open(PUBLIC_SONARR_BASE_URL + '/series/' + $itemStore.item?.sonarrSeries?.titleSlug)} + window.open( + env.PUBLIC_SONARR_BASE_URL + '/series/' + $itemStore.item?.sonarrSeries?.titleSlug + )} > Open in Sonarr diff --git a/src/lib/components/VideoPlayer/VideoPlayer.svelte b/src/lib/components/VideoPlayer/VideoPlayer.svelte index f627ff2..9892881 100644 --- a/src/lib/components/VideoPlayer/VideoPlayer.svelte +++ b/src/lib/components/VideoPlayer/VideoPlayer.svelte @@ -31,7 +31,10 @@ itemId, getDeviceProfile(), item?.UserData?.PlaybackPositionTicks || 0 - ).then(async ({ playbackUri, playSessionId: sessionId, mediaSourceId, directPlay }) => { + ).then(async (playbackInfo) => { + if (!playbackInfo) return; + const { playbackUri, playSessionId: sessionId, mediaSourceId, directPlay } = playbackInfo; + if (!playbackUri || !sessionId) { console.log('No playback URL or session ID', playbackUri, sessionId); return; diff --git a/src/lib/stores/library.store.ts b/src/lib/stores/library.store.ts index 7c867c7..63804b2 100644 --- a/src/lib/stores/library.store.ts +++ b/src/lib/stores/library.store.ts @@ -76,11 +76,12 @@ async function getLibrary(): Promise { const sonarrSeries = await sonarrSeriesPromise; const sonarrDownloads = await sonarrDownloadsPromise; - const jellyfinContinueWatching = await jellyfinContinueWatchingPromise; - const jellyfinLibraryItems = await jellyfinLibraryItemsPromise; - const jellyfinEpisodes = await jellyfinEpisodesPromise.then((episodes) => - episodes?.sort((a, b) => (a.IndexNumber || 99) - (b.IndexNumber || 99)) - ); + const jellyfinContinueWatching = (await jellyfinContinueWatchingPromise) || []; + const jellyfinLibraryItems = (await jellyfinLibraryItemsPromise) || []; + const jellyfinEpisodes = + (await jellyfinEpisodesPromise.then((episodes) => + episodes?.sort((a, b) => (a.IndexNumber || 99) - (b.IndexNumber || 99)) + )) || []; const jellyfinNextUp = await jellyfinNextUpPromise; const items: Record = {}; diff --git a/src/routes/+layout.svelte b/src/routes/+layout.svelte index cd3e75e..ff074ea 100644 --- a/src/routes/+layout.svelte +++ b/src/routes/+layout.svelte @@ -1,5 +1,6 @@ -{#if data.isApplicationSetUp} -
- -
- -
- {#key $page.url.pathname} - - {/key} - -
-{:else} - -{/if} + +
+ +
+ +
+ {#key $page.url.pathname} + + {/key} + +
+ + + diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f6ba6d1..11e9e04 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -7,6 +7,8 @@ import { library } from '$lib/stores/library.store'; import type { ComponentProps } from 'svelte'; + let continueWatchingVisible = true; + const tmdbPopularMoviesPromise = getTmdbPopularMovies() .then((movies) => Promise.all(movies.map((movie) => getTmdbMovie(movie.id || 0)))) .then((movies) => movies.filter((m) => !!m).slice(0, 10)); @@ -44,6 +46,12 @@ ) ); + continueWatchingProps.then((props) => { + if (props.length === 0) { + continueWatchingVisible = false; + } + }); + let showcaseIndex = 0; async function onNext() { @@ -83,7 +91,7 @@ {/await} -
+