diff --git a/src/app.css b/src/app.css index f942941..08ccc49 100644 --- a/src/app.css +++ b/src/app.css @@ -15,9 +15,9 @@ a { } .selectable { - @apply focus-within:outline outline-2 outline-[#FDEA8A88] outline-offset-2; + @apply focus-visible:outline outline-2 outline-[#f0cd6dc2] outline-offset-2; } -.selectable-tab { - @apply focus-visible:outline outline-2 outline-[#FDEA8A88] outline-offset-2; +.selectable-explicit { + @apply focus-within:outline outline-2 outline-[#f0cd6dc2] outline-offset-2; } diff --git a/src/lib/apis/jellyfin/jellyfinApi.ts b/src/lib/apis/jellyfin/jellyfinApi.ts index e3dd571..d13d327 100644 --- a/src/lib/apis/jellyfin/jellyfinApi.ts +++ b/src/lib/apis/jellyfin/jellyfinApi.ts @@ -16,20 +16,29 @@ export const JellyfinApi = createClient({ } }); -export const getJellyfinContinueWatching = (limit = 8): Promise => +export const getJellyfinContinueWatching = (): Promise => JellyfinApi.get('/Users/{userId}/Items/Resume', { params: { path: { userId: JELLYFIN_USER_ID }, query: { - limit, mediaTypes: ['Video'], fields: ['ProviderIds'] } } }).then((r) => r.data?.Items || []); +export const getJellyfinNextUp = () => + JellyfinApi.get('/Shows/NextUp', { + params: { + query: { + userId: JELLYFIN_USER_ID, + fields: ['ProviderIds'] + } + } + }).then((r) => r.data?.Items || []); + export const getJellyfinItems = () => JellyfinApi.get('/Users/{userId}/Items', { params: { diff --git a/src/lib/components/Carousel/Carousel.svelte b/src/lib/components/Carousel/Carousel.svelte index 849a5d1..c7000b3 100644 --- a/src/lib/components/Carousel/Carousel.svelte +++ b/src/lib/components/Carousel/Carousel.svelte @@ -4,13 +4,16 @@ import { ChevronLeft, ChevronRight } from 'radix-icons-svelte'; export let gradientFromColor = 'from-stone-900'; + export let heading = ''; let carousel: HTMLDivElement | undefined; let scrollX = 0;
- + +
{heading}
+
{ diff --git a/src/lib/components/EpisodeCard/EpisodeCard.svelte b/src/lib/components/EpisodeCard/EpisodeCard.svelte index 846c306..a8b5550 100644 --- a/src/lib/components/EpisodeCard/EpisodeCard.svelte +++ b/src/lib/components/EpisodeCard/EpisodeCard.svelte @@ -5,6 +5,8 @@ import { fade } from 'svelte/transition'; import IconButton from '../IconButton.svelte'; import { onMount } from 'svelte'; + import PlayButton from '../PlayButton.svelte'; + import ProgressBar from '../ProgressBar.svelte'; export let backdropPath: string; @@ -66,12 +68,12 @@
{#if subtitle} -
{subtitle}
+

{subtitle}

{/if} {#if title} -
+

{title} -

+ {/if}
@@ -83,19 +85,16 @@
-
- - - -
+
{#if progress}
-
+
{/if} diff --git a/src/lib/components/Navbar/Navbar.svelte b/src/lib/components/Navbar/Navbar.svelte index 5557f5e..8ae947a 100644 --- a/src/lib/components/Navbar/Navbar.svelte +++ b/src/lib/components/Navbar/Navbar.svelte @@ -12,7 +12,7 @@ let isSearchVisible = false; function getLinkStyle(path: string) { - return classNames('selectable-tab rounded-sm px-2 -mx-2', { + return classNames('selectable rounded-sm px-2 -mx-2', { 'text-amber-200': $page.url.pathname === path, 'hover:text-zinc-50 cursor-pointer': $page.url.pathname !== path }); @@ -35,10 +35,7 @@
- +

Reiverr

diff --git a/src/lib/components/PlayButton.svelte b/src/lib/components/PlayButton.svelte new file mode 100644 index 0000000..24bce28 --- /dev/null +++ b/src/lib/components/PlayButton.svelte @@ -0,0 +1,11 @@ + + +
+ + + +
diff --git a/src/lib/components/Poster/Poster.svelte b/src/lib/components/Poster/Poster.svelte index e17693f..9f3005a 100644 --- a/src/lib/components/Poster/Poster.svelte +++ b/src/lib/components/Poster/Poster.svelte @@ -5,94 +5,73 @@ import { formatMinutesToTime } from '$lib/utils'; import { onMount } from 'svelte'; import { playerState } from '../VideoPlayer/VideoPlayer'; + import classNames from 'classnames'; + import PlayButton from '../PlayButton.svelte'; + import ProgressBar from '../ProgressBar.svelte'; + + export let tmdbId: number; + export let jellyfinId: string = ''; + export let type: 'movie' | 'series' = 'movie'; + export let backdropUri: string; + + export let title = ''; + export let subtitle = ''; - export let tmdbId: string; export let progress = 0; - export let length = 0; - export let randomProgress = false; - if (randomProgress) progress = Math.random() > 0.5 ? Math.round(Math.random() * 100) : 100; - - export let type: 'movie' | 'tv' = 'movie'; - - let bg = ''; - let title = 'Loading...'; - - onMount(() => { - TmdbApi.get('/' + type + '/' + tmdbId) - .then((res) => res.data) - .then((data: any) => { - bg = TMDB_POSTER_SMALL + data.poster_path; - title = data.title; - }); - }); - - let streamFetching = false; - function stream() { - if (streamFetching || !tmdbId) return; - streamFetching = true; - getJellyfinItemByTmdbId(tmdbId).then((item: any) => { - if (item.Id) playerState.streamJellyfinId(item.Id); - streamFetching = false; - }); - } -
+
-
-
- -
- -
(window.location.href = '/' + type + '/' + tmdbId)} - > + + + +
+ +
+
+ +
+ + +
+
-
-
+
+ { + e.preventDefault(); + jellyfinId && playerState.streamJellyfinId(jellyfinId); + }} + class="opacity-0 group-hover:opacity-100 transition-opacity" + /> +
+ {#if progress} +
+ +
+ {/if} + diff --git a/src/lib/components/ProgressBar.svelte b/src/lib/components/ProgressBar.svelte new file mode 100644 index 0000000..94ba297 --- /dev/null +++ b/src/lib/components/ProgressBar.svelte @@ -0,0 +1,7 @@ + + +
+
+
diff --git a/src/lib/components/ResourceDetails/ResourceDetails.svelte b/src/lib/components/ResourceDetails/ResourceDetails.svelte index 1ed1122..987d6f7 100644 --- a/src/lib/components/ResourceDetails/ResourceDetails.svelte +++ b/src/lib/components/ResourceDetails/ResourceDetails.svelte @@ -172,11 +172,13 @@ {/if}

{reason}

-

+

{title}

diff --git a/src/lib/components/TitlePageLayout.svelte b/src/lib/components/TitlePageLayout.svelte index 49cc0e3..e3af4fa 100644 --- a/src/lib/components/TitlePageLayout.svelte +++ b/src/lib/components/TitlePageLayout.svelte @@ -13,10 +13,10 @@ export let overview: string; -
+
diff --git a/src/lib/stores/library.store.ts b/src/lib/stores/library.store.ts index c090a5b..69dca14 100644 --- a/src/lib/stores/library.store.ts +++ b/src/lib/stores/library.store.ts @@ -1,8 +1,8 @@ import { getJellyfinContinueWatching, getJellyfinEpisodes, - getJellyfinEpisodesBySeries, getJellyfinItems, + getJellyfinNextUp, type JellyfinItem } from '$lib/apis/jellyfin/jellyfinApi'; import { @@ -19,10 +19,10 @@ import { } from '$lib/apis/sonarr/sonarrApi'; import { getTmdbMovie, - getTmdbMovieBackdrop, getTmdbSeries, - getTmdbSeriesBackdrop, - getTmdbSeriesFromTvdbId + getTmdbSeriesFromTvdbId, + type TmdbMovieFull2, + type TmdbSeriesFull2 } from '$lib/apis/tmdb/tmdbApi'; import { get, writable } from 'svelte/store'; import { settings } from './settings.store'; @@ -45,10 +45,13 @@ export interface PlayableItem { tmdbId: number; jellyfinItem?: JellyfinItem; jellyfinEpisodes?: JellyfinItem[]; + nextJellyfinEpisode?: JellyfinItem; radarrMovie?: RadarrMovie; radarrDownloads?: RadarrDownload[]; sonarrSeries?: SonarrSeries; sonarrDownloads?: SonarrDownload[]; + tmdbMovie?: TmdbMovieFull2; + tmdbSeries?: TmdbSeriesFull2; } export interface Library { @@ -64,7 +67,8 @@ async function getLibrary(): Promise { const sonarrSeriesPromise = getSonarrSeries(); const sonarrDownloadsPromise = getSonarrDownloads(); - const continueWatchingPromise = getJellyfinContinueWatching(); + const jellyfinContinueWatchingPromise = getJellyfinContinueWatching(); + const jellyfinNextUpPromise = getJellyfinNextUp(); const jellyfinLibraryItemsPromise = getJellyfinItems(); const jellyfinEpisodesPromise = getJellyfinEpisodes(); @@ -74,11 +78,12 @@ async function getLibrary(): Promise { const sonarrSeries = await sonarrSeriesPromise; const sonarrDownloads = await sonarrDownloadsPromise; - const jellyfinContinueWatching = await continueWatchingPromise; + 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 = {}; @@ -112,15 +117,13 @@ async function getLibrary(): Promise { ? { length, progress: watchingProgress } : undefined; - const backdropUrl = await getTmdbMovie(radarrMovie.tmdbId || 0).then( - (r) => - ( - r?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) || - r?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') || - r?.images?.backdrops?.find((b) => b.iso_639_1) || - r?.images?.backdrops?.[0] - )?.file_path - ); + const tmdbMovie = await getTmdbMovie(radarrMovie.tmdbId || 0); + const backdropUrl = ( + tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) || + tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') || + tmdbMovie?.images?.backdrops?.find((b) => b.iso_639_1) || + tmdbMovie?.images?.backdrops?.[0] + )?.file_path; return { type: 'movie' as const, @@ -133,7 +136,8 @@ async function getLibrary(): Promise { jellyfinId: jellyfinItem?.Id, jellyfinItem, radarrMovie, - radarrDownloads: itemRadarrDownloads + radarrDownloads: itemRadarrDownloads, + tmdbMovie }; }); @@ -156,14 +160,17 @@ async function getLibrary(): Promise { ? { progress: downloadProgress, completionTime } : undefined; - const length = jellyfinItem?.RunTimeTicks - ? jellyfinItem.RunTimeTicks / 10_000_000 / 60 + const nextJellyfinEpisode = jellyfinItem + ? jellyfinContinueWatching.find((i) => i.SeriesId === jellyfinItem?.Id) || + jellyfinNextUp.find((i) => i.SeriesId === jellyfinItem?.Id) : undefined; - const watchingProgress = jellyfinItem?.UserData?.PlayedPercentage; + + const length = nextJellyfinEpisode?.RunTimeTicks + ? nextJellyfinEpisode.RunTimeTicks / 10_000_000 / 60 + : undefined; + const watchingProgress = nextJellyfinEpisode?.UserData?.PlayedPercentage; const continueWatching = - length && - watchingProgress && - !!jellyfinContinueWatching.find((i) => i.Id === jellyfinItem?.Id) + length && watchingProgress && !!nextJellyfinEpisode ? { length, progress: watchingProgress } : undefined; @@ -172,15 +179,13 @@ async function getLibrary(): Promise { : undefined; const tmdbId = tmdbItem?.id || undefined; - const backdropUrl = await getTmdbSeries(tmdbId || 0).then( - (r) => - ( - r?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) || - r?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') || - r?.images?.backdrops?.find((b) => b.iso_639_1) || - r?.images?.backdrops?.[0] - )?.file_path - ); + const tmdbSeries = await getTmdbSeries(tmdbId || 0); + const backdropUrl = ( + tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) || + tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1 === 'en') || + tmdbSeries?.images?.backdrops?.find((b) => b.iso_639_1) || + tmdbSeries?.images?.backdrops?.[0] + )?.file_path; return { type: 'series' as const, @@ -194,7 +199,9 @@ async function getLibrary(): Promise { jellyfinItem, sonarrSeries, sonarrDownloads: itemSonarrDownloads, - jellyfinEpisodes: jellyfinEpisodes.filter((i) => i.SeriesId === jellyfinItem?.Id) + jellyfinEpisodes: jellyfinEpisodes.filter((i) => i.SeriesId === jellyfinItem?.Id), + nextJellyfinEpisode, + tmdbSeries }; }); @@ -207,7 +214,9 @@ async function getLibrary(): Promise { return { items, itemsArray: Object.values(items), - continueWatching: Object.values(items).filter((i) => i.continueWatching) + continueWatching: Object.values(items).filter( + (i) => i.continueWatching || i.nextJellyfinEpisode + ) }; } diff --git a/src/routes/+page.svelte b/src/routes/+page.svelte index f2c657f..ce64bbd 100644 --- a/src/routes/+page.svelte +++ b/src/routes/+page.svelte @@ -1,33 +1,69 @@ -{#await moviesPromise} +{#await tmdbPopularMoviesPromise}
{:then movies} - {@const movie = movies[index]} + {@const movie = movies[showcaseIndex]} g.name || '') || []} runtime={movie?.runtime || 0} tmdbRating={movie?.vote_average || 0} - starring={movie?.credits?.cast?.slice(0, 5)} + starring={movie?.credits?.cast?.slice(0, 5) || []} videos={movie.videos?.results || []} backdropPath={movie?.backdrop_path || ''} > @@ -46,7 +82,7 @@ slot="page-controls" {onNext} {onPrevious} - {index} + index={showcaseIndex} length={movies.length} /> @@ -54,20 +90,20 @@ Error occurred {JSON.stringify(err)} {/await} -{#await $library then libraryData} - {#if libraryData.itemsArray.filter((item) => item.continueWatching).length} - {@const continueWatching = libraryData.continueWatching} -
-

Continue Watching

-
- {#each continueWatching.slice(0, 5) as item (item.tmdbId)} - - {/each} -
-
- {/if} -{/await} +
+ + {#await continueWatchingProps} + + {:then props} + {#each props as prop} + +
+ {#if prop.progress} + {(prop.runtime - (prop.runtime / 100) * prop.progress).toFixed()} Minutes Left + {/if} +
+
+ {/each} + {/await} +
+
diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index dff9d7e..4e2bcf1 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -168,7 +168,7 @@
document.activeElement !== searchInput && searchInput?.focus()} diff --git a/src/routes/movie/[id]/MoviePage.svelte b/src/routes/movie/[id]/MoviePage.svelte index ec25c4e..c8de94b 100644 --- a/src/routes/movie/[id]/MoviePage.svelte +++ b/src/routes/movie/[id]/MoviePage.svelte @@ -18,6 +18,7 @@ import { formatMinutesToTime, formatSize } from '$lib/utils'; import { Archive, ChevronRight, Plus } from 'radix-icons-svelte'; import type { ComponentProps } from 'svelte'; + import ProgressBar from '$lib/components/ProgressBar.svelte'; export let tmdbId: number; const tmdbUrl = 'https://www.themoviedb.org/movie/' + tmdbId; @@ -81,10 +82,8 @@ {@const progress = $itemStore.item?.continueWatching?.progress} {#if progress} -
-
+
+
{/if}