Files
reiverr/src/routes/+page.svelte
2023-09-23 15:14:30 +03:00

311 lines
8.9 KiB
Svelte

<script lang="ts">
import {
getJellyfinBackdrop,
getJellyfinContinueWatching,
getJellyfinNextUp,
type JellyfinItem
} from '$lib/apis/jellyfin/jellyfinApi';
import {
getPosterProps,
getTmdbMovie,
getTmdbPopularMovies,
TmdbApiOpen
} from '$lib/apis/tmdb/tmdbApi';
import Carousel from '$lib/components/Carousel/Carousel.svelte';
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
import GenreCard from '$lib/components/GenreCard.svelte';
import NetworkCard from '$lib/components/NetworkCard.svelte';
import PersonCard from '$lib/components/PersonCard/PersonCard.svelte';
import Poster from '$lib/components/Poster/Poster.svelte';
import TitleShowcases from '$lib/components/TitleShowcase/TitleShowcasesContainer.svelte';
import { genres, networks } from '$lib/discover';
import { jellyfinItemsStore } from '$lib/stores/data.store';
import { settings } from '$lib/stores/settings.store';
import type { TitleType } from '$lib/types';
import { formatDateToYearMonthDay } from '$lib/utils';
import type { ComponentProps } from 'svelte';
import { _ } from 'svelte-i18n';
import { fade } from 'svelte/transition';
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));
let nextUpP = getJellyfinNextUp();
let continueWatchingP = getJellyfinContinueWatching();
let nextUpProps = Promise.all([nextUpP, continueWatchingP])
.then(([nextUp, continueWatching]) => [
...(continueWatching || []),
...(nextUp?.filter((i) => !continueWatching?.find((c) => c.SeriesId === i.SeriesId)) || [])
])
.then((items) =>
Promise.all(
items?.map(async (item) => {
const parentSeries = await jellyfinItemsStore.promise.then((items) =>
items.find((i) => i.Id === item.SeriesId)
);
return {
tmdbId: Number(item.ProviderIds?.Tmdb) || Number(parentSeries?.ProviderIds?.Tmdb) || 0,
jellyfinId: item.Id,
backdropUrl: getJellyfinBackdrop(item),
title: item.Name || '',
progress: item.UserData?.PlayedPercentage || undefined,
runtime: item.RunTimeTicks ? item.RunTimeTicks / 10_000_000 / 60 : 0,
...(item.Type === 'Movie'
? {
type: 'movie',
subtitle: item.Genres?.join(', ') || ''
}
: {
type: 'series',
subtitle:
(item?.IndexNumber && 'Episode ' + item.IndexNumber) ||
item.Genres?.join(', ') ||
''
})
} as const;
})
)
);
nextUpProps.then((props) => {
if (props.length === 0) {
continueWatchingVisible = false;
}
});
let showcaseIndex = 0;
async function onNext() {
showcaseIndex = (showcaseIndex + 1) % (await tmdbPopularMoviesPromise).length;
}
async function onPrevious() {
showcaseIndex =
(showcaseIndex - 1 + (await tmdbPopularMoviesPromise).length) %
(await tmdbPopularMoviesPromise).length;
}
const jellyfinItemsPromise = new Promise<JellyfinItem[]>((resolve) => {
jellyfinItemsStore.subscribe((data) => {
if (data.loading) return;
resolve(data.data || []);
});
});
const fetchCardProps = async (
items: {
name?: string;
title?: string;
id?: number;
vote_average?: number;
number_of_seasons?: number;
first_air_date?: string;
poster_path?: string;
}[],
type: TitleType | undefined = undefined
): Promise<ComponentProps<Poster>[]> => {
const filtered = $settings.discover.excludeLibraryItems
? items.filter(
async (item) =>
!(await jellyfinItemsPromise).find((i) => i.ProviderIds?.Tmdb === String(item.id))
)
: items;
return Promise.all(filtered.map(async (item) => getPosterProps(item, type))).then((props) =>
props.filter((p) => p.backdropUrl)
);
};
const trendingItemsPromise = TmdbApiOpen.get('/3/trending/all/{time_window}', {
params: {
path: {
time_window: 'day'
},
query: {
language: $settings.language
}
}
}).then((res) => res.data?.results || []);
const fetchTrendingProps = () => trendingItemsPromise.then(fetchCardProps);
const fetchTrendingActorProps = () =>
TmdbApiOpen.get('/3/trending/person/{time_window}', {
params: {
path: {
time_window: 'week'
}
}
})
.then((res) => res.data?.results || [])
.then((actors) =>
actors
.filter((a) => a.profile_path)
.map((actor) => ({
tmdbId: actor.id || 0,
backdropUri: actor.profile_path || '',
name: actor.name || '',
subtitle: actor.known_for_department || ''
}))
);
const fetchUpcomingMovies = () =>
TmdbApiOpen.get('/3/discover/movie', {
params: {
query: {
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
sort_by: 'popularity.desc',
language: $settings.language,
region: $settings.discover.region,
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
}
}
})
.then((res) => res.data?.results || [])
.then(fetchCardProps);
const fetchUpcomingSeries = () =>
TmdbApiOpen.get('/3/discover/tv', {
params: {
query: {
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
sort_by: 'popularity.desc',
language: $settings.language,
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
}
}
})
.then((res) => res.data?.results || [])
.then((i) => fetchCardProps(i, 'series'));
const fetchDigitalReleases = () =>
TmdbApiOpen.get('/3/discover/movie', {
params: {
query: {
with_release_type: 4,
sort_by: 'popularity.desc',
'release_date.lte': formatDateToYearMonthDay(new Date()),
language: $settings.language,
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
// region: $settings.discover.region
}
}
})
.then((res) => res.data?.results || [])
.then(fetchCardProps);
const fetchNowStreaming = () =>
TmdbApiOpen.get('/3/discover/tv', {
params: {
query: {
'air_date.gte': formatDateToYearMonthDay(new Date()),
'first_air_date.lte': formatDateToYearMonthDay(new Date()),
sort_by: 'popularity.desc',
language: $settings.language,
with_original_language: parseIncludedLanguages($settings.discover.includedLanguages)
}
}
})
.then((res) => res.data?.results || [])
.then((i) => fetchCardProps(i, 'series'));
function parseIncludedLanguages(includedLanguages: string) {
return includedLanguages.replace(' ', '').split(',').join('|');
}
const PADDING = 'px-4 lg:px-8 2xl:px-16';
</script>
<TitleShowcases />
<div
class="flex flex-col gap-12 py-6 bg-stone-950"
in:fade|global={{
duration: $settings.animationDuration,
delay: $settings.animationDuration
}}
out:fade|global={{ duration: $settings.animationDuration }}
>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.popularPeople')}
</div>
{#await fetchTrendingActorProps()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop (prop.tmdbId)}
<PersonCard {...prop} />
{/each}
{/await}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.upcomingMovies')}
</div>
{#await fetchUpcomingMovies()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop (prop.tmdbId)}
<Poster {...prop} />
{/each}
{/await}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.upcomingSeries')}
</div>
{#await fetchUpcomingSeries()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop (prop.tmdbId)}
<Poster {...prop} />
{/each}
{/await}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.genres')}
</div>
{#each Object.values(genres) as genre (genre.tmdbGenreId)}
<GenreCard {genre} />
{/each}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.newDigitalReleases')}
</div>
{#await fetchDigitalReleases()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop (prop.tmdbId)}
<Poster {...prop} />
{/each}
{/await}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.streamingNow')}
</div>
{#await fetchNowStreaming()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop (prop.tmdbId)}
<Poster {...prop} />
{/each}
{/await}
</Carousel>
<Carousel scrollClass={PADDING}>
<div slot="title" class="text-lg font-semibold text-zinc-300">
{$_('discover.TVNetworks')}
</div>
{#each Object.values(networks) as network (network.tmdbNetworkId)}
<NetworkCard {network} />
{/each}
</Carousel>
</div>