Completed discovery page

This commit is contained in:
Aleksi Lassila
2023-07-31 00:09:07 +03:00
parent 77153a96c5
commit 8e60c8fad2
37 changed files with 876 additions and 325 deletions

View File

@@ -178,15 +178,24 @@ export const getSonarrEpisodes = async (seriesId: number) => {
}));
};
export const fetchSonarrReleases = async (episodeId: number) => {
return SonarrApi.get('/api/v3/release', {
export const fetchSonarrReleases = async (episodeId: number) =>
SonarrApi.get('/api/v3/release', {
params: {
query: {
episodeId
}
}
}).then((r) => r.data || []);
};
export const fetchSonarrSeasonReleases = async (seriesId: number, seasonNumber: number) =>
SonarrApi.get('/api/v3/release', {
params: {
query: {
seriesId,
seasonNumber
}
}
}).then((r) => r.data || []);
export const fetchSonarrEpisodes = async (seriesId: number): Promise<SonarrEpisode[]> => {
return SonarrApi.get('/api/v3/episode', {

View File

@@ -1,24 +1,30 @@
import axios from 'axios';
import { PUBLIC_TMDB_API_KEY } from '$env/static/public';
import { request } from '$lib/utils';
import { formatDateToYearMonthDay, request } from '$lib/utils';
import type { operations, paths } from './tmdb.generated';
import createClient from 'openapi-fetch';
import { get } from 'svelte/store';
import { getIncludedLanguagesQuery, settings } from '$lib/stores/settings.store';
import type { ComponentProps } from 'svelte';
import type PeopleCard from '$lib/components/PeopleCard/PeopleCard.svelte';
export type SeriesDetails =
export type TmdbMovie2 =
operations['movie-details']['responses']['200']['content']['application/json'];
export type TmdbSeries2 =
operations['tv-series-details']['responses']['200']['content']['application/json'];
export type SeasonDetails =
export type TmdbSeason =
operations['tv-season-details']['responses']['200']['content']['application/json'];
export interface SeriesDetailsFull extends SeriesDetails {
videos: {
results: Video[];
};
credits: {
cast: CastMember[];
};
external_ids: {
imdb_id?: string;
tvdb_id?: number;
};
export interface TmdbMovieFull2 extends TmdbMovie2 {
videos: operations['movie-videos']['responses']['200']['content']['application/json'];
credits: operations['movie-credits']['responses']['200']['content']['application/json'];
external_ids: operations['movie-external-ids']['responses']['200']['content']['application/json'];
}
export interface TmdbSeriesFull2 extends TmdbSeries2 {
videos: operations['tv-series-videos']['responses']['200']['content']['application/json'];
credits: operations['tv-series-credits']['responses']['200']['content']['application/json'];
external_ids: operations['tv-series-external-ids']['responses']['200']['content']['application/json'];
}
export const TmdbApiOpen = createClient<paths>({
@@ -35,17 +41,12 @@ export const getTmdbMovie = async (tmdbId: number) =>
movie_id: tmdbId
},
query: {
append_to_response: 'videos,credits'
append_to_response: 'videos,credits,external_ids'
}
}
}).then((res) => res.data as TmdbMovieFull | undefined);
}).then((res) => res.data as TmdbMovieFull2 | undefined);
export const getTmdbPopularMovies = () =>
TmdbApiOpen.get('/3/movie/popular', {
params: {}
}).then((res) => res.data?.results || []);
export const getTmdbSeriesFromTvdbId = async (tvdbId: number): Promise<any> =>
export const getTmdbSeriesFromTvdbId = async (tvdbId: number) =>
TmdbApiOpen.get('/3/find/{external_id}', {
params: {
path: {
@@ -58,12 +59,12 @@ export const getTmdbSeriesFromTvdbId = async (tvdbId: number): Promise<any> =>
headers: {
'Cache-Control': 'max-age=86400'
}
}).then((res) => res.data?.tv_results?.[0]);
}).then((res) => res.data?.tv_results?.[0] as TmdbSeries2 | undefined);
export const getTmdbIdFromTvdbId = async (tvdbId: number) =>
getTmdbSeriesFromTvdbId(tvdbId).then((res: any) => res?.id as number | undefined);
export const getTmdbSeries = async (tmdbId: number): Promise<SeriesDetailsFull | undefined> =>
export const getTmdbSeries = async (tmdbId: number): Promise<TmdbSeriesFull2 | undefined> =>
await TmdbApiOpen.get('/3/tv/{series_id}', {
params: {
path: {
@@ -73,12 +74,12 @@ export const getTmdbSeries = async (tmdbId: number): Promise<SeriesDetailsFull |
append_to_response: 'videos,credits,external_ids'
}
}
}).then((res) => res.data as SeriesDetailsFull | undefined);
}).then((res) => res.data as TmdbSeriesFull2 | undefined);
export const getTmdbSeriesSeason = async (
tmdbId: number,
season: number
): Promise<SeasonDetails | undefined> =>
): Promise<TmdbSeason | undefined> =>
TmdbApiOpen.get('/3/tv/{series_id}/season/{season_number}', {
params: {
path: {
@@ -90,7 +91,7 @@ export const getTmdbSeriesSeason = async (
export const getTmdbSeriesSeasons = async (tmdbId: number, seasons: number) =>
Promise.all([...Array(seasons).keys()].map((i) => getTmdbSeriesSeason(tmdbId, i + 1))).then(
(r) => r.filter((s) => s) as SeasonDetails[]
(r) => r.filter((s) => s) as TmdbSeason[]
);
export const getTmdbSeriesImages = async (tmdbId: number) =>
@@ -101,12 +102,128 @@ export const getTmdbSeriesImages = async (tmdbId: number) =>
}
},
headers: {
'Cache-Control': 'max-age=86400'
'Cache-Control': 'max-age=345600' // 4 days
}
}).then((res) => res.data);
export const getTmdbSeriesBackdrop = async (tmdbId: number) =>
getTmdbSeriesImages(tmdbId).then(
(r) =>
(
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
r?.backdrops?.find((b) => b.iso_639_1) ||
r?.backdrops?.[0]
)?.file_path
);
export const getTmdbMovieImages = async (tmdbId: number) =>
await TmdbApiOpen.get('/3/movie/{movie_id}/images', {
params: {
path: {
movie_id: tmdbId
}
},
headers: {
'Cache-Control': 'max-age=345600' // 4 days
}
}).then((res) => res.data);
export const getTmdbMovieBackdrop = async (tmdbId: number) =>
getTmdbMovieImages(tmdbId).then(
(r) =>
(
r?.backdrops?.find((b) => b.iso_639_1 === get(settings).language) ||
r?.backdrops?.find((b) => b.iso_639_1 === 'en') ||
r?.backdrops?.find((b) => b.iso_639_1) ||
r?.backdrops?.[0]
)?.file_path
);
export const getTmdbPopularMovies = () =>
TmdbApiOpen.get('/3/movie/popular', {
params: {
query: {
language: get(settings).language,
region: get(settings).region
}
}
}).then((res) => res.data?.results || []);
export const getTmdbPopularSeries = () =>
TmdbApiOpen.get('/3/tv/popular', {
params: {
query: {
language: get(settings).language
}
}
}).then((res) => res.data?.results || []);
export const getTmdbTrendingAll = () =>
TmdbApiOpen.get('/3/trending/all/{time_window}', {
params: {
path: {
time_window: 'day'
},
query: {
language: get(settings).language
}
}
}).then((res) => res.data?.results || []);
export const getTmdbNetworkSeries = (networkId: number) =>
TmdbApiOpen.get('/3/discover/tv', {
params: {
query: {
with_networks: networkId
}
}
}).then((res) => res.data?.results || []);
export const getTmdbDigitalReleases = () =>
TmdbApiOpen.get('/3/discover/movie', {
params: {
query: {
with_release_type: 4,
sort_by: 'popularity.desc',
...getIncludedLanguagesQuery()
}
}
}).then((res) => res.data?.results || []);
export const getTmdbUpcomingMovies = () =>
TmdbApiOpen.get('/3/discover/movie', {
params: {
query: {
'primary_release_date.gte': formatDateToYearMonthDay(new Date()),
sort_by: 'popularity.desc',
...getIncludedLanguagesQuery()
}
}
}).then((res) => res.data?.results || []);
export const getTrendingActors = () =>
TmdbApiOpen.get('/3/trending/person/{time_window}', {
params: {
path: {
time_window: 'week'
}
}
}).then((res) => res.data?.results || []);
export const getTmdbGenreMovies = (genreId: number) =>
TmdbApiOpen.get('/3/discover/movie', {
params: {
query: {
with_genres: String(genreId),
...getIncludedLanguagesQuery()
}
}
}).then((res) => res.data?.results || []);
// Deprecated hereon forward
/** @deprecated */
export const TmdbApi = axios.create({
baseURL: 'https://api.themoviedb.org/3',
headers: {
@@ -114,27 +231,26 @@ export const TmdbApi = axios.create({
}
});
/** @deprecated */
export const fetchTmdbMovie = async (tmdbId: string): Promise<TmdbMovie> =>
await TmdbApi.get<TmdbMovie>('/movie/' + tmdbId).then((r) => r.data);
/** @deprecated */
export const fetchTmdbMovieVideos = async (tmdbId: string): Promise<Video[]> =>
await TmdbApi.get<VideosResponse>('/movie/' + tmdbId + '/videos').then((res) => res.data.results);
/** @deprecated */
export const fetchTmdbMovieImages = async (tmdbId: string): Promise<ImagesResponse> =>
await TmdbApi.get<ImagesResponse>('/movie/' + tmdbId + '/images', {
headers: {
'Cache-Control': 'max-age=86400'
'Cache-Control': 'max-age=345600' // 4 days
}
}).then((res) => res.data);
/** @deprecated */
export const fetchTmdbMovieCredits = async (tmdbId: string): Promise<CastMember[]> =>
await TmdbApi.get<CreditsResponse>('/movie/' + tmdbId + '/credits').then((res) => res.data.cast);
export const fetchTmdbPopularMovies = () =>
TmdbApi.get<PopularMoviesResponse>('/movie/popular').then((res) => res.data.results);
export const requestTmdbPopularMovies = () => request(fetchTmdbPopularMovies, null);
export interface TmdbMovieFull extends TmdbMovie {
videos: {
results: Video[];

View File

@@ -4,14 +4,14 @@
import { TMDB_IMAGES } from '$lib/constants';
import { Clock, Star } from 'radix-icons-svelte';
export let tmdbId: string;
export let tmdbId: number;
export let type: 'movie' | 'series' = 'movie';
export let title: string;
export let genres: string[];
export let genres: string[] = [];
export let runtimeMinutes = 0;
export let seasons = 0;
export let completionTime = '';
export let backdropUrl: string;
export let backdropUri: string;
export let rating: number;
export let available = true;
@@ -23,22 +23,19 @@
}
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<div
tabindex={0}
<a
class={classNames(
'rounded overflow-hidden relative shadow-2xl shrink-0 aspect-video selectable',
'rounded overflow-hidden relative shadow-lg shrink-0 aspect-video selectable block hover:text-inherit',
{
'h-40': size === 'md',
'h-60': size === 'lg',
'w-full': size === 'dynamic'
}
)}
href={`/${type}/${tmdbId}`}
>
<div style={'width: ' + progress + '%'} class="h-[2px] bg-zinc-200 bottom-0 absolute z-[1]" />
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
on:click={() => window.open(`/${type}/${tmdbId}`, '_self')}
class="h-full w-full opacity-0 hover:opacity-100 transition-opacity flex flex-col justify-between cursor-pointer p-2 px-3 relative z-[1] peer"
style={progress > 0 ? 'padding-bottom: 0.6rem;' : ''}
>
@@ -82,7 +79,7 @@
</div>
</div>
<div
style={"background-image: url('" + TMDB_IMAGES + backdropUrl + "')"}
style={"background-image: url('" + TMDB_IMAGES + backdropUri + "')"}
class="absolute inset-0 bg-center bg-cover peer-hover:scale-105 transition-transform"
/>
<div
@@ -91,4 +88,4 @@
'bg-[#00000055] peer-hover:bg-darken': !available
})}
/>
</div>
</a>

View File

@@ -0,0 +1,3 @@
<div class="grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5">
<slot />
</div>

View File

@@ -1,42 +1,47 @@
import type { RadarrMovie } from '$lib/apis/radarr/radarrApi';
import { fetchTmdbMovieImages } from '$lib/apis/tmdb/tmdbApi';
import type { TmdbMovie } from '$lib/apis/tmdb/tmdbApi';
import {
fetchTmdbMovieImages,
getTmdbMovieBackdrop,
getTmdbMovieImages,
getTmdbSeriesBackdrop,
getTmdbSeriesImages
} from '$lib/apis/tmdb/tmdbApi';
import type { TmdbMovie, TmdbMovie2, TmdbSeries2 } from '$lib/apis/tmdb/tmdbApi';
import type { ComponentProps } from 'svelte';
import type Card from './Card.svelte';
export interface CardProps {
tmdbId: string;
title: string;
genres: string[];
runtimeMinutes: number;
backdropUrl: string;
rating: number;
}
export const fetchCardProps = async (movie: RadarrMovie): Promise<CardProps> => {
const backdropUrl = fetchTmdbMovieImages(String(movie.tmdbId)).then(
(r) => r.backdrops.filter((b) => b.iso_639_1 === 'en')[0].file_path
);
export const fetchCardTmdbMovieProps = async (movie: TmdbMovie2): Promise<ComponentProps<Card>> => {
const backdropUri = getTmdbMovieBackdrop(movie.id || 0);
return {
tmdbId: String(movie.tmdbId),
title: String(movie.title),
genres: movie.genres as string[],
runtimeMinutes: movie.runtime as any,
backdropUrl: await backdropUrl,
rating: movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0
};
};
export const fetchCardPropsTmdb = async (movie: TmdbMovie): Promise<CardProps> => {
const backdropUrl = fetchTmdbMovieImages(String(movie.id))
.then((r) => r.backdrops.filter((b) => b.iso_639_1 === 'en')[0]?.file_path)
.catch(console.error);
return {
tmdbId: String(movie.id),
title: String(movie.original_title),
genres: movie.genres.map((g) => g.name),
tmdbId: movie.id || 0,
title: movie.title || '',
genres: movie.genres?.map((g) => g.name || '') || [],
runtimeMinutes: movie.runtime,
backdropUrl: (await backdropUrl) || '',
backdropUri: (await backdropUri) || '',
rating: movie.vote_average || 0
};
};
export const fetchCardTmdbSeriesProps = async (
series: TmdbSeries2
): Promise<ComponentProps<Card>> => {
const backdropUri = getTmdbSeriesBackdrop(series.id || 0);
return {
tmdbId: series.id || 0,
title: series.name || '',
genres: series.genres?.map((g) => g.name || '') || [],
runtimeMinutes: series.episode_run_time?.[0],
backdropUri: (await backdropUri) || '',
rating: series.vote_average || 0,
type: 'series'
};
};
export const fetchCardTmdbProps = async (
item: TmdbSeries2 | TmdbMovie2
): Promise<ComponentProps<Card>> => {
if ('name' in item) return fetchCardTmdbSeriesProps(item);
return fetchCardTmdbMovieProps(item);
};

View File

@@ -14,14 +14,14 @@
<div class="flex gap-2">
<IconButton
on:click={() => {
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth, behavior: 'smooth' });
carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' });
}}
>
<ChevronLeft size={20} />
</IconButton>
<IconButton
on:click={() => {
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth, behavior: 'smooth' });
carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' });
}}
>
<ChevronRight size={20} />

View File

@@ -0,0 +1,27 @@
<script lang="ts">
import type { Genre } from '$lib/discover';
import { capitalize } from '$lib/utils';
export let genre: Genre;
</script>
<!-- svelte-ignore a11y-no-noninteractive-tabindex -->
<a
class="rounded-xl overflow-hidden relative shadow-2xl shrink-0 aspect-[21/9] selectable h-40 block"
href={`/discover/genre/${genre.name}`}
>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<!-- svelte-ignore a11y-click-events-have-key-events -->
<div
class="h-full w-full flex flex-col items-center justify-center cursor-pointer p-2 px-3 relative z-[1] peer"
>
<h1 class="font-bold text-2xl tracking-wider">
{capitalize(genre.name)}
</h1>
</div>
<div
style={"background-image: url('/genres/" + genre.name + ".jpg')"}
class="absolute inset-0 bg-center bg-cover peer-hover:scale-105 transition-transform"
/>
<div class="absolute inset-0 bg-darken bg-opacity-60" />
</a>

View File

@@ -0,0 +1,34 @@
<script lang="ts">
import { ChevronLeft } from 'radix-icons-svelte';
import IconButton from '../IconButton.svelte';
import CardGrid from '../Card/CardGrid.svelte';
import CardPlaceholder from '../Card/CardPlaceholder.svelte';
import { capitalize } from '$lib/utils';
export let title: string;
</script>
<div class="pt-24 p-8 bg-black">
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div class="flex flex-col gap-1 items-start">
<!-- svelte-ignore a11y-click-events-have-key-events -->
<h1 class="font-bold text-5xl">{capitalize(title)}</h1>
<div
class="flex items-center cursor-pointer hover:text-zinc-200 text-zinc-400 transition-colors"
on:click={() => window?.history?.back()}
>
<ChevronLeft size={20} />
<h2>Back</h2>
</div>
</div>
</div>
<div class="p-8">
<CardGrid>
<slot>
{#each [...Array(20).keys()] as index (index)}
<CardPlaceholder size="dynamic" {index} />
{/each}
</slot>
</CardGrid>
</div>

View File

@@ -0,0 +1,17 @@
<script lang="ts">
import type { Network } from '$lib/discover';
export let network: Network;
</script>
<a
href={`/discover/network/${network.name}`}
class="border rounded-xl h-52 w-96 bg-stone-800 border-stone-700 cursor-pointer p-12 text-zinc-300 hover:text-amber-200 transition-all relative group selectable"
>
<div
class="absolute inset-10 bg-zinc-300 hover:bg-amber-200 group-hover:scale-105 transition-all"
style={"mask-image: url('/networks/" +
network.name +
".svg'); mask-size: contain; mask-repeat: no-repeat; mask-position: center;"}
/>
</a>

View File

@@ -0,0 +1,45 @@
<script lang="ts">
import { TMDB_IMAGES } from '$lib/constants';
import classNames from 'classnames';
export let tmdbId: number;
export let knownFor: string[];
export let name: string;
export let backdropUri: string;
export let department: string;
export let size: 'dynamic' | 'md' | 'lg' = 'lg';
</script>
<a
class={classNames(
'rounded-xl overflow-hidden relative shadow-lg shrink-0 aspect-[4/5] selectable block hover:text-inherit',
{
'h-40': size === 'md',
'h-52': size === 'lg',
'w-full': size === 'dynamic'
}
)}
href={`/person/${tmdbId}`}
>
<div
class="h-full w-full transition-opacity flex flex-col justify-between cursor-pointer relative z-[1] peer group"
>
<div class="opacity-0 group-hover:opacity-100">
<!-- <h2 class="text-sm text-zinc-300 tracking-wider font-medium line-clamp-2">
{knownFor.join(', ')}
</h2> -->
</div>
<div class="bg-gradient-to-t from-darken from-20% to-transparent p-2 px-3 pt-8">
<h2
class="text-xs text-zinc-300 tracking-wider font-medium opacity-0 group-hover:opacity-100"
>
{department}
</h2>
<h1 class="font-bold tracking-wider text-lg">{name}</h1>
</div>
</div>
<div
style={"background-image: url('" + TMDB_IMAGES + backdropUri + "')"}
class="absolute inset-0 bg-center bg-cover peer-hover:scale-105 transition-transform"
/>
</a>

115
src/lib/discover.ts Normal file
View File

@@ -0,0 +1,115 @@
export interface Network {
name: string;
tmdbNetworkId: number;
}
export const networks: Record<string, Network> = {
netflix: {
name: 'netflix',
tmdbNetworkId: 213
},
disney: {
name: 'disney',
tmdbNetworkId: 2739
},
hbo: {
name: 'hbo',
tmdbNetworkId: 49
},
hulu: {
name: 'hulu',
tmdbNetworkId: 453
},
amazon: {
name: 'amazon',
tmdbNetworkId: 1024
},
apple: {
name: 'apple',
tmdbNetworkId: 2552
}
};
export interface Genre {
name: string;
tmdbGenreId: number;
}
export const genres: Record<string, Genre> = {
action: {
name: 'action',
tmdbGenreId: 28
},
adventure: {
name: 'adventure',
tmdbGenreId: 12
},
animation: {
name: 'animation',
tmdbGenreId: 16
},
comedy: {
name: 'comedy',
tmdbGenreId: 35
},
crime: {
name: 'crime',
tmdbGenreId: 80
},
documentary: {
name: 'documentary',
tmdbGenreId: 99
},
drama: {
name: 'drama',
tmdbGenreId: 18
},
family: {
name: 'family',
tmdbGenreId: 10751
},
fantasy: {
name: 'fantasy',
tmdbGenreId: 14
},
history: {
name: 'history',
tmdbGenreId: 36
},
horror: {
name: 'horror',
tmdbGenreId: 27
},
music: {
name: 'music',
tmdbGenreId: 10402
},
mystery: {
name: 'mystery',
tmdbGenreId: 9648
},
romance: {
name: 'romance',
tmdbGenreId: 10749
},
scienceFiction: {
name: 'scienceFiction',
tmdbGenreId: 878
},
tvMovie: {
name: 'tvMovie',
tmdbGenreId: 10770
},
thriller: {
name: 'thriller',
tmdbGenreId: 53
},
war: {
name: 'war',
tmdbGenreId: 10752
},
western: {
name: 'western',
tmdbGenreId: 37
}
};

View File

@@ -17,10 +17,12 @@ import {
} from '$lib/apis/sonarr/sonarrApi';
import {
fetchTmdbMovieImages,
getTmdbMovieBackdrop,
getTmdbSeriesBackdrop,
getTmdbSeriesFromTvdbId,
getTmdbSeriesImages
} from '$lib/apis/tmdb/tmdbApi';
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
export interface PlayableItem {
type: 'movie' | 'series';
@@ -101,9 +103,7 @@ async function getLibrary(): Promise<Library> {
? { length, progress: watchingProgress }
: undefined;
const backdropUrl = await fetchTmdbMovieImages(String(radarrMovie.tmdbId)).then(
(r) => r.backdrops.find((b) => b.iso_639_1 === 'en')?.file_path
);
const backdropUrl = await getTmdbMovieBackdrop(radarrMovie.tmdbId || 0);
return {
type: 'movie' as const,
@@ -155,16 +155,12 @@ async function getLibrary(): Promise<Library> {
: undefined;
const tmdbId = tmdbItem?.id || undefined;
const backdropUrl = tmdbId
? await getTmdbSeriesImages(tmdbId).then(
(r) => r?.backdrops?.find((b) => b.iso_639_1 === 'en')?.file_path
)
: undefined;
const backdropUrl = tmdbId ? await getTmdbSeriesBackdrop(tmdbId) : undefined;
return {
type: 'series' as const,
tmdbId,
tmdbRating: tmdbItem.vote_average || 0,
tmdbRating: tmdbItem?.vote_average || 0,
cardBackdropUrl: backdropUrl || '',
download,
continueWatching,
@@ -192,9 +188,16 @@ async function getLibrary(): Promise<Library> {
function createLibraryStore() {
const { update, set, ...library } = writable<Promise<Library>>(getLibrary()); //TODO promise to undefined
async function filterNotInLibrary<T extends any>(toFilter: T[], getTmdbId: (item: T) => any) {
const libraryData = await get(library);
return toFilter.filter((item) => !(getTmdbId(item) in libraryData.items));
}
return {
...library,
refresh: async () => getLibrary().then((r) => set(Promise.resolve(r)))
refresh: async () => getLibrary().then((r) => set(Promise.resolve(r))),
filterNotInLibrary
};
}

View File

@@ -1,11 +1,34 @@
import { writable } from 'svelte/store';
import { get, writable } from 'svelte/store';
interface Settings {
autoplayTrailers: boolean;
excludeLibraryItemsFromDiscovery: boolean;
language: string;
region: string;
discover: {
includedLanguages: string[];
filterBasedOnLanguage: boolean;
};
}
const defaultSettings: Settings = {
autoplayTrailers: true
autoplayTrailers: true,
excludeLibraryItemsFromDiscovery: true,
language: 'en',
region: 'US',
discover: {
filterBasedOnLanguage: true,
includedLanguages: ['en']
}
};
export const settings = writable<Settings>(defaultSettings);
export const getIncludedLanguagesQuery = () => {
const settingsValue = get(settings);
if (settingsValue.discover.filterBasedOnLanguage) {
return { with_original_language: settingsValue.language };
}
return {};
};

View File

@@ -79,3 +79,16 @@ export function log(arg: any) {
console.log('LOGGER', arg);
return arg;
}
export function formatDateToYearMonthDay(date: Date) {
const year = date.getFullYear();
const month = String(date.getMonth() + 1).padStart(2, '0');
const day = String(date.getDate()).padStart(2, '0');
return `${year}-${month}-${day}`;
}
export function capitalize(str: string) {
const strings = str.split(' ');
return strings.map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(' ');
}