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(' ');
}

View File

@@ -1,6 +1,5 @@
import { getTmdbMovie, getTmdbPopularMovies, TmdbApi } from '$lib/apis/tmdb/tmdbApi';
import type { TmdbMovie } from '$lib/apis/tmdb/tmdbApi';
import { getJellyfinContinueWatching } from '$lib/apis/jellyfin/jellyfinApi';
import { getTmdbPopularMovies } from '$lib/apis/tmdb/tmdbApi';
import type { PageServerLoad } from './$types';
export const load = (async () => {

View File

@@ -1,19 +0,0 @@
import type { PageServerLoad } from './$types';
import { fetchTmdbMovie, fetchTmdbPopularMovies } from '$lib/apis/tmdb/tmdbApi';
import { fetchCardPropsTmdb } from '$lib/components/Card/card';
export const load = (() => {
const popularMoviesPromise = fetchTmdbPopularMovies();
const popularMovies = popularMoviesPromise.then((movies) => {
return Promise.all(
movies.map(async (movie) => fetchCardPropsTmdb(await fetchTmdbMovie(String(movie.id))))
);
});
return {
streamed: {
popularMovies
}
};
}) satisfies PageServerLoad;

View File

@@ -1,80 +1,166 @@
<script lang="ts">
import { getTmdbMovie, fetchTmdbPopularMovies, fetchTmdbMovie } from '$lib/apis/tmdb/tmdbApi';
import {
TmdbApiOpen,
getTmdbDigitalReleases,
getTmdbTrendingAll,
getTmdbUpcomingMovies,
getTrendingActors,
type TmdbMovie2,
type TmdbSeries2
} from '$lib/apis/tmdb/tmdbApi';
import Card from '$lib/components/Card/Card.svelte';
import { fetchCardPropsTmdb } from '$lib/components/Card/card';
import { fetchCardTmdbProps } from '$lib/components/Card/card';
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 PeopleCard from '$lib/components/PeopleCard/PeopleCard.svelte';
import { genres, networks } from '$lib/discover';
import { library } from '$lib/stores/library.store';
import AmazonCard from './AmazonCard.svelte';
import DisneyCard from './DisneyCard.svelte';
import HboCard from './HboCard.svelte';
import HuluCard from './HuluCard.svelte';
import NetflixCard from './NetflixCard.svelte';
import { getIncludedLanguagesQuery, settings } from '$lib/stores/settings.store';
import { formatDateToYearMonthDay } from '$lib/utils';
async function getDiscoverData() {
const popularMoviesPromise = fetchTmdbPopularMovies();
const fetchCardProps = async (items: TmdbMovie2[] | TmdbSeries2[]) =>
Promise.all(
(
await ($settings.excludeLibraryItemsFromDiscovery
? library.filterNotInLibrary(items, (t) => t.id)
: items)
).map(fetchCardTmdbProps)
).then((props) => props.filter((p) => p.backdropUri));
const popularMovies = await popularMoviesPromise
.then(async (tmdbMovies) => {
const libraryData = await $library;
return tmdbMovies.filter((m) => !libraryData.items[m.id]);
})
.then((tmdbMovies) => {
return Promise.all(
tmdbMovies.map(async (tmdbMovie) =>
fetchCardPropsTmdb(await fetchTmdbMovie(String(tmdbMovie.id)))
)
);
});
const fetchTrendingProps = () => getTmdbTrendingAll().then(fetchCardProps);
const fetchDigitalReleases = () => getTmdbDigitalReleases().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',
...getIncludedLanguagesQuery()
}
}
})
.then((res) => res.data?.results || [])
.then(fetchCardProps);
const fetchUpcomingMovies = () => getTmdbUpcomingMovies().then(fetchCardProps);
const fetchUpcomingSeries = () =>
TmdbApiOpen.get('/3/discover/tv', {
params: {
query: {
'first_air_date.gte': formatDateToYearMonthDay(new Date()),
sort_by: 'popularity.desc',
...getIncludedLanguagesQuery()
}
}
})
.then((res) => res.data?.results || [])
.then(fetchCardProps);
return {
popularMovies
};
}
const discoverPromise = getDiscoverData();
const fetchTrendingActors = () =>
getTrendingActors().then((actors) =>
actors
.filter((a) => a.profile_path)
.map((actor) => ({
tmdbId: actor.id || 0,
name: actor.name || '',
backdropUri: actor.profile_path || '',
knownFor: actor.known_for?.map((movie) => movie.title || '') || [],
department: actor.known_for_department || ''
}))
);
const headerStyle = 'uppercase tracking-widest font-bold';
</script>
<div class="pb-24 flex flex-col gap-4">
<!-- Does not contain any of the titles in library.-->
<div class="pt-24 bg-black">
<Carousel gradientFromColor="from-black">
<div slot="title" class={headerStyle}>For You</div>
{#await discoverPromise}
<div slot="title" class={headerStyle}>Trending</div>
{#await fetchTrendingProps()}
<CarouselPlaceholderItems size="lg" />
{:then { popularMovies: movies }}
{#each movies ? [...movies].reverse() : [] as movie (movie.tmdbId)}
<Card size="lg" {...movie} />
{:then props}
{#each props as prop}
<Card size="lg" {...prop} />
{/each}
{/await}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>Popular Movies</div>
{#await discoverPromise}
<div slot="title" class={headerStyle}>Popular People</div>
{#await fetchTrendingActors()}
<CarouselPlaceholderItems />
{:then { popularMovies: movies }}
{#each movies || [] as movie (movie.tmdbId)}
<Card {...movie} />
{:then props}
{#each props as prop}
<PeopleCard {...prop} />
{/each}
{/await}
</Carousel>
</div>
<!-- <div class={headerStyle}>Popular TV Shows</div>-->
<div>
<Carousel>
<div slot="title" class={headerStyle}>Networks</div>
<NetflixCard />
<HboCard />
<DisneyCard />
<AmazonCard />
<!-- <AppleCard />-->
<HuluCard />
<div slot="title" class={headerStyle}>Upcoming Movies</div>
{#await fetchUpcomingMovies()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop}
<Card {...prop} />
{/each}
{/await}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>Upcoming Series</div>
{#await fetchUpcomingSeries()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop}
<Card {...prop} />
{/each}
{/await}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>Genres</div>
{#each Object.values(genres) as genre}
<GenreCard {genre} />
{/each}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>New Digital Releeases</div>
{#await fetchDigitalReleases()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop}
<Card {...prop} />
{/each}
{/await}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>Streaming Now</div>
{#await fetchNowStreaming()}
<CarouselPlaceholderItems />
{:then props}
{#each props as prop}
<Card {...prop} />
{/each}
{/await}
</Carousel>
</div>
<div>
<Carousel>
<div slot="title" class={headerStyle}>TV Networks</div>
{#each Object.values(networks) as network}
<NetworkCard {network} />
{/each}
</Carousel>
</div>
<!-- <div class={headerStyle}>Categories</div>-->
</div>

View File

@@ -1,55 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/amazon">
<svg
class="scale-[200%]"
version="1.1"
id="Layer_1"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px"
y="0px"
width="800px"
height="50px"
viewBox="0 0 80 50"
style="enable-background:new 0 0 80 50;"
xml:space="preserve"
>
<style type="text/css">
.st0 {
fill: currentColor;
}
</style>
<title>PrimeLogo_Blue</title>
<path
class="st0"
d="M1.15,29.2c-0.215,0.01-0.426-0.061-0.59-0.2c-0.14-0.167-0.205-0.384-0.18-0.6V8 C0.355,7.784,0.42,7.567,0.56,7.4c0.166-0.126,0.372-0.186,0.58-0.17h2.22C3.78,7.188,4.158,7.483,4.22,7.9l0.22,0.8 c0.639-0.617,1.394-1.103,2.22-1.43c0.845-0.342,1.748-0.519,2.66-0.52c1.818-0.06,3.556,0.749,4.68,2.18 c1.238,1.706,1.853,3.785,1.74,5.89c0.034,1.528-0.259,3.045-0.86,4.45c-0.496,1.17-1.302,2.183-2.33,2.93 c-0.994,0.678-2.177,1.028-3.38,1c-0.817,0.004-1.629-0.131-2.4-0.4c-0.709-0.238-1.364-0.612-1.93-1.1v6.72 c0.022,0.214-0.038,0.429-0.17,0.6c-0.171,0.132-0.386,0.192-0.6,0.17L1.15,29.2z M7.88,19.84c0.988,0.071,1.944-0.371,2.53-1.17 c0.623-1.118,0.905-2.394,0.81-3.67c0.096-1.288-0.182-2.576-0.8-3.71c-0.588-0.807-1.554-1.25-2.55-1.17 c-1.057,0.001-2.093,0.287-3,0.83V19c0.9,0.56,1.94,0.854,3,0.85L7.88,19.84z"
/>
<path
class="st0"
d="M19.54,22.88c-0.358,0.067-0.703-0.169-0.77-0.528c-0.015-0.08-0.015-0.162,0-0.242V8 c-0.025-0.216,0.04-0.433,0.18-0.6c0.166-0.126,0.372-0.186,0.58-0.17h2.21c0.42-0.042,0.798,0.253,0.86,0.67L23,9.54 c0.657-0.769,1.442-1.419,2.32-1.92c0.716-0.375,1.512-0.57,2.32-0.57h0.43c0.217-0.019,0.434,0.041,0.61,0.17 c0.14,0.167,0.205,0.384,0.18,0.6v2.58c0.017,0.208-0.043,0.414-0.17,0.58c-0.167,0.14-0.384,0.205-0.6,0.18h-0.55 c-0.227,0-0.513,0-0.86,0c-0.578,0.012-1.154,0.079-1.72,0.2c-0.59,0.109-1.166,0.28-1.72,0.51v10.25 c0.016,0.208-0.044,0.414-0.17,0.58c-0.167,0.14-0.384,0.205-0.6,0.18L19.54,22.88z"
/>
<path
class="st0"
d="M33.29,4.79c-0.682,0.034-1.351-0.195-1.87-0.64c-0.482-0.451-0.741-1.091-0.71-1.75 c-0.034-0.663,0.225-1.307,0.71-1.76c1.096-0.877,2.654-0.877,3.75,0c0.482,0.451,0.741,1.091,0.71,1.75 c0.031,0.659-0.228,1.299-0.71,1.75C34.65,4.591,33.977,4.824,33.29,4.79z M31.82,22.89c-0.358,0.067-0.703-0.169-0.77-0.528 c-0.015-0.08-0.015-0.162,0-0.242V8c-0.025-0.216,0.04-0.433,0.18-0.6c0.166-0.126,0.372-0.186,0.58-0.17h2.95 c0.214-0.022,0.429,0.038,0.6,0.17c0.132,0.171,0.192,0.386,0.17,0.6v14.12c0.017,0.208-0.043,0.414-0.17,0.58 c-0.167,0.14-0.384,0.205-0.6,0.18L31.82,22.89z"
/>
<path
class="st0"
d="M40.1,22.88c-0.358,0.067-0.703-0.169-0.77-0.528c-0.015-0.08-0.015-0.162,0-0.242V8 c-0.025-0.216,0.04-0.433,0.18-0.6c0.166-0.126,0.372-0.186,0.58-0.17h2.21c0.42-0.042,0.798,0.253,0.86,0.67l0.25,0.83 c0.91-0.627,1.894-1.137,2.93-1.52c0.855-0.298,1.754-0.453,2.66-0.46c1.576-0.135,3.09,0.642,3.9,2 c0.913-0.628,1.905-1.132,2.95-1.5c0.922-0.307,1.888-0.462,2.86-0.46c1.228-0.074,2.432,0.36,3.33,1.2 c0.828,0.908,1.254,2.113,1.18,3.34v10.79c0.016,0.208-0.044,0.414-0.17,0.58c-0.167,0.14-0.384,0.205-0.6,0.18h-2.91 c-0.358,0.067-0.703-0.169-0.77-0.528c-0.015-0.08-0.015-0.162,0-0.242v-9.84c0-1.393-0.623-2.09-1.87-2.09 c-1.162,0.013-2.307,0.287-3.35,0.8v11.14c0.017,0.208-0.043,0.414-0.17,0.58c-0.167,0.14-0.384,0.205-0.6,0.18h-2.94 c-0.358,0.067-0.703-0.169-0.77-0.528c-0.015-0.08-0.015-0.162,0-0.242v-9.84c0-1.393-0.623-2.09-1.87-2.09 c-1.176,0.007-2.334,0.292-3.38,0.83v11.1c0.016,0.208-0.044,0.414-0.17,0.58c-0.167,0.14-0.384,0.205-0.6,0.18L40.1,22.88z"
/>
<path
class="st0"
d="M73.92,23.34c-2.155,0.141-4.272-0.615-5.85-2.09c-1.443-1.652-2.164-3.813-2-6 c-0.144-2.267,0.586-4.504,2.04-6.25c1.515-1.564,3.636-2.393,5.81-2.27c1.608-0.096,3.196,0.395,4.47,1.38 c1.078,0.92,1.672,2.285,1.61,3.7c0.073,1.385-0.588,2.707-1.74,3.48c-1.551,0.886-3.328,1.296-5.11,1.18 c-1.01,0.012-2.018-0.102-3-0.34c0.006,1.106,0.452,2.164,1.24,2.94c0.933,0.662,2.069,0.977,3.21,0.89 c0.558,0,1.116-0.037,1.67-0.11c0.762-0.118,1.516-0.279,2.26-0.48h0.18h0.15c0.347,0,0.52,0.237,0.52,0.71v1.41 c0.019,0.239-0.029,0.478-0.14,0.69c-0.142,0.167-0.33,0.288-0.54,0.35C77.17,23.093,75.55,23.368,73.92,23.34z M72.92,13.71 c0.787,0.057,1.574-0.109,2.27-0.48c0.478-0.327,0.748-0.882,0.71-1.46c0-1.287-0.767-1.93-2.3-1.93c-1.967,0-3.103,1.207-3.41,3.62 c0.893,0.171,1.801,0.255,2.71,0.25H72.92z"
/>
<path
class="st0"
d="M72.31,40.11C63.55,46.57,50.85,50,39.92,50c-14.606,0.078-28.716-5.295-39.57-15.07 c-0.82-0.74-0.09-1.75,0.9-1.18c12.058,6.885,25.705,10.501,39.59,10.49c10.361-0.055,20.61-2.152,30.16-6.17 C72.52,37.44,73.76,39,72.31,40.11z"
/>
<path
class="st0"
d="M76,36c-1.12-1.43-7.4-0.68-10.23-0.34c-0.86,0.1-1-0.64-0.22-1.18c5-3.52,13.23-2.5,14.18-1.32 s-0.25,9.41-5,13.34c-0.72,0.6-1.41,0.28-1.09-0.52C74.71,43.29,77.08,37.39,76,36z"
/>
</svg>
</NetworkCard>

View File

@@ -1,11 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/apple">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 45.046 17.091" width="100%" height="100%"
><path
d="M9.436,2.742A3.857,3.857,0,0,0,10.316,0a3.769,3.769,0,0,0-2.51,1.311,3.622,3.622,0,0,0-.9,2.631,3.138,3.138,0,0,0,2.53-1.2m.82,1.381c-1.4-.081-2.58.8-3.25.8s-1.69-.756-2.79-.736a4.117,4.117,0,0,0-3.5,2.147c-1.5,2.6-.4,6.473,1.06,8.59.71,1.006,1.56,2.205,2.69,2.166s1.48-.7,2.77-.7,1.67.7,2.79.675,1.9-1.008,2.6-2.1a9.317,9.317,0,0,0,1.17-2.42,3.814,3.814,0,0,1-2.27-3.468,3.9,3.9,0,0,1,1.83-3.256,3.991,3.991,0,0,0-3.1-1.7m8.93-2.016V4.96h2.28V6.845h-2.28V13.6c0,1.008.45,1.522,1.45,1.522a7.482,7.482,0,0,0,.82-.06v1.9a7.823,7.823,0,0,1-1.35.1c-2.36,0-3.27-.917-3.27-3.216V6.89h-1.79V5h1.74V2.107Zm10.25,14.853h-2.5L22.736,5h2.49l2.95,9.608h.06L31.186,5h2.44Zm10.98,0h-2.16v-4.9h-4.64V9.9h4.63V5h2.16V9.9h4.64v2.158h-4.63Z"
/></svg
>
</NetworkCard>

View File

@@ -1,76 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/disney">
<svg
width="1041px"
height="565px"
viewBox="0 0 1041 565"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
version="1.1"
>
<!-- Generated by Pixelmator Pro 2.2 -->
<g id="Layer_1">
<g id="group">
<g id="group-1">
<g id="group-2">
<path
id="Path"
d="M735.8 365.7 C721.4 369 683.5 370.9 683.5 370.9 L678.7 385.9 C678.7 385.9 697.6 384.3 711.4 385.7 711.4 385.7 715.9 385.2 716.4 390.8 716.6 396 716 401.6 716 401.6 716 401.6 715.7 405 710.9 405.8 705.7 406.7 670.1 408 670.1 408 L664.3 427.5 C664.3 427.5 662.2 432 667 430.7 671.5 429.5 708.8 422.5 713.7 423.5 718.9 424.8 724.7 431.7 723 438.1 721 445.9 683.8 469.7 661.1 468 661.1 468 649.2 468.8 639.1 452.7 629.7 437.4 642.7 408.3 642.7 408.3 642.7 408.3 636.8 394.7 641.1 390.2 641.1 390.2 643.7 387.9 651.1 387.3 L660.2 368.4 C660.2 368.4 649.8 369.1 643.6 361.5 637.8 354.2 637.4 350.9 641.8 348.9 646.5 346.6 689.8 338.7 719.6 339.7 719.6 339.7 730 338.7 738.9 356.7 738.8 356.7 743.2 364 735.8 365.7 Z M623.7 438.3 C619.9 447.3 609.8 456.9 597.3 450.9 584.9 444.9 565.2 404.6 565.2 404.6 565.2 404.6 557.7 389.6 556.3 389.9 556.3 389.9 554.7 387 553.7 403.4 552.7 419.8 553.9 451.7 547.4 456.7 541.2 461.7 533.7 459.7 529.8 453.8 526.3 448 524.8 434.2 526.7 410 529 385.8 534.6 360 541.8 351.9 549 343.9 554.8 349.7 557 351.8 557 351.8 566.6 360.5 582.5 386.1 L585.3 390.8 C585.3 390.8 599.7 415 601.2 414.9 601.2 414.9 602.4 416 603.4 415.2 604.9 414.8 604.3 407 604.3 407 604.3 407 601.3 380.7 588.2 336.1 588.2 336.1 586.2 330.5 587.6 325.3 588.9 320 594.2 322.5 594.2 322.5 594.2 322.5 614.6 332.7 624.4 365.9 634.1 399.4 627.5 429.3 623.7 438.3 Z M523.5 353 C521.8 356.4 520.8 361.3 512.2 362.6 512.2 362.6 429.9 368.2 426 374 426 374 423.1 377.4 427.6 378.4 432.1 379.3 450.7 381.8 459.7 382.3 469.3 382.4 501.7 382.7 513.3 397.2 513.3 397.2 520.2 404.1 519.9 419.7 519.6 435.7 516.8 441.3 510.6 447.1 504.1 452.5 448.3 477.5 412.3 439.1 412.3 439.1 395.7 420.6 418 406.6 418 406.6 434.1 396.9 475 408.3 475 408.3 487.4 412.8 486.8 417.3 486.1 422.1 476.6 427.2 462.8 426.9 449.4 426.5 439.6 420.1 441.5 421.1 443.3 421.8 427.1 413.3 422.1 419.1 417.1 424.4 418.3 427.7 423.2 431 435.7 438.1 484 435.6 498.4 419.6 498.4 419.6 504.1 413.1 495.4 407.8 486.7 402.8 461.8 399.8 452.1 399.3 442.8 398.8 408.2 399.4 403.2 390.2 403.2 390.2 398.2 384 403.7 366.4 409.5 348 449.8 340.9 467.2 339.3 467.2 339.3 515.1 337.6 523.9 347.4 523.8 347.4 525 349.7 523.5 353 Z M387.5 460.9 C381.7 465.2 369.4 463.3 365.9 458.5 362.4 454.2 361.2 437.1 361.9 410.3 362.6 383.2 363.2 349.6 369 344.3 375.2 338.9 379 343.6 381.4 347.3 384 350.9 387.1 354.9 387.8 363.4 388.4 371.9 390.4 416.5 390.4 416.5 390.4 416.5 393 456.7 387.5 460.9 Z M400 317.1 C383.1 322.7 371.5 320.8 361.7 316.6 357.4 324.1 354.9 326.4 351.6 326.9 346.8 327.4 342.5 319.7 341.7 317.2 340.9 315.3 338.6 312.1 341.4 304.5 331.8 295.9 331.1 284.3 332.7 276.5 335.1 267.5 351.3 233.3 400.6 229.3 400.6 229.3 424.7 227.5 428.8 240.4 L429.5 240.4 C429.5 240.4 452.9 240.5 452.4 261.3 452.1 282.2 426.4 308.2 400 317.1 Z M354 270.8 C349 278.8 348.8 283.6 351.1 286.9 356.8 278.2 367.2 264.5 382.5 254.1 370.7 255.1 360.8 260.2 354 270.8 Z M422.1 257.4 C406.6 259.7 382.6 280.5 371.2 297.5 388.7 300.7 419.6 299.5 433.3 271.6 433.2 271.6 439.8 254.3 422.1 257.4 Z M842.9 418.5 C833.6 434.7 807.5 468.5 772.7 460.6 761.2 488.5 751.6 516.6 746.1 558.8 746.1 558.8 744.9 567 738.1 564.1 731.4 561.7 720.2 550.5 718 535 715.6 514.6 724.7 480.1 743.2 440.6 737.8 431.8 734.1 419.2 737.3 401.3 737.3 401.3 742 368.1 775.3 338.1 775.3 338.1 779.3 334.6 781.6 335.7 784.2 336.8 783 347.6 780.9 352.8 778.8 358 763.9 383.8 763.9 383.8 763.9 383.8 754.6 401.2 757.2 414.9 774.7 388 814.5 333.7 839.2 350.8 847.5 356.7 851.3 369.6 851.3 383.5 851.2 395.8 848.3 408.8 842.9 418.5 Z M835.7 375.9 C835.7 375.9 834.3 365.2 823.9 377 814.9 386.9 798.7 405.6 785.6 430.9 799.3 429.4 812.5 421.9 816.5 418.1 823 412.3 838.1 396.7 835.7 375.9 Z M350.2 389.5 C348.3 413.7 339 454.4 273.1 474.5 229.6 487.6 188.5 481.3 166.1 475.6 165.6 484.5 164.6 488.3 163.2 489.8 161.3 491.7 147.1 499.9 139.3 488.3 135.8 482.8 134 472.8 133 463.9 82.6 440.7 59.4 407.3 58.5 405.8 57.4 404.7 45.9 392.7 57.4 378 68.2 364.7 103.5 351.4 135.3 346 136.4 318.8 139.6 298.3 143.4 288.9 148 278 153.8 287.8 158.8 295.2 163 300.7 165.5 324.4 165.7 343.3 186.5 342.3 198.8 343.8 222 348 252.2 353.5 272.4 368.9 270.6 386.4 269.3 403.6 253.5 410.7 247.5 411.2 241.2 411.7 231.4 407.2 231.4 407.2 224.7 404 230.9 401.2 239 397.7 247.8 393.4 245.8 389 245.8 389 242.5 379.4 203.3 372.7 164.3 372.7 164.1 394.2 165.2 429.9 165.7 450.7 193 455.9 213.4 454.9 213.4 454.9 213.4 454.9 313 452.1 316 388.5 319.1 324.8 216.7 263.7 141 244.3 65.4 224.5 22.6 238.3 18.9 240.2 14.9 242.2 18.6 242.8 18.6 242.8 18.6 242.8 22.7 243.4 29.8 245.8 37.3 248.2 31.5 252.1 31.5 252.1 18.6 256.2 4.1 253.6 1.3 247.7 -1.5 241.8 3.2 236.5 8.6 228.9 14 220.9 19.9 221.2 19.9 221.2 113.4 188.8 227.3 247.4 227.3 247.4 334 301.5 352.2 364.9 350.2 389.5 Z M68 386.2 C57.4 391.4 64.7 398.9 64.7 398.9 84.6 420.3 109.1 433.7 132.4 442 135.1 405.1 134.7 392.1 135 373.5 98.6 376 77.6 381.8 68 386.2 Z"
fill="currentColor"
fill-opacity="1"
stroke="none"
/>
</g>
</g>
<g id="group-3">
<g id="group-4">
<g id="group-5">
<path
id="Path-1"
d="M1040.9 378.6 L1040.9 391.8 C1040.9 394.7 1038.6 397 1035.7 397 L972.8 397 C972.8 400.3 972.9 403.2 972.9 405.9 972.9 425.4 972.1 441.3 970.2 459.2 969.9 461.9 967.7 463.9 965.1 463.9 L951.5 463.9 C950.1 463.9 948.8 463.3 947.9 462.3 947 461.3 946.5 459.9 946.7 458.5 948.6 440.7 949.5 425 949.5 405.9 949.5 403.1 949.5 400.2 949.4 397 L887.2 397 C884.3 397 882 394.7 882 391.8 L882 378.6 C882 375.7 884.3 373.4 887.2 373.4 L948.5 373.4 C947.2 351.9 944.6 331.2 940.4 310.2 940.2 308.9 940.5 307.6 941.3 306.6 942.1 305.6 943.3 305 944.6 305 L959.3 305 C961.6 305 963.5 306.6 964 308.9 968.1 330.6 970.7 351.7 972 373.4 L1035.7 373.4 C1038.5 373.4 1040.9 375.8 1040.9 378.6 Z"
fill="currentColor"
fill-opacity="1"
stroke="none"
/>
</g>
</g>
</g>
<g id="group-6">
<g id="group-7">
<path
id="Path-2"
d="M200.2 204.3 L200.1 204.3 M199.4 204.4 C199.1 204.4 198.8 204.3 198.5 204.3 198.8 204.4 199.1 204.4 199.4 204.4 L199.7 204.4 C199.6 204.4 199.5 204.4 199.4 204.4 Z M199.4 204.4 C199.1 204.4 198.8 204.3 198.5 204.3 198.8 204.4 199.1 204.4 199.4 204.4 L199.7 204.4 C199.6 204.4 199.5 204.4 199.4 204.4 Z"
fill="none"
stroke="none"
/>
<defs>
<radialGradient
id="radial-gradient"
gradientUnits="userSpaceOnUse"
cx="942.524"
cy="279.896"
r="760.124"
fx="942.524"
fy="279.896"
>
<stop offset="0.007" stop-color="currentColor" stop-opacity="1" />
<stop offset="0.216" stop-color="currentColor" stop-opacity="1" />
<stop offset="1" stop-color="currentColor" stop-opacity="0" />
</radialGradient>
</defs>
<path
id="Path-3"
d="M955.3 273.9 C922.8 194 867.9 125.9 796.5 76.9 723.4 26.8 637.7 0.3 548.7 0.3 401.5 0.3 264.9 73.4 183.4 195.9 182.5 197.2 182.3 198.9 182.8 200.4 183.3 202 184.5 203.1 186 203.6 L197.4 207.5 C198.1 207.7 198.8 207.8 199.4 207.8 201.5 207.8 203.5 206.7 204.7 205 242.1 150 292.7 104.3 351.1 72.7 411.4 40.1 479.7 22.8 548.6 22.8 631.9 22.8 712.2 47.4 781 93.8 848.1 139.1 900.2 202.4 931.7 276.7 932.6 278.9 934.8 280.4 937.2 280.4 L950.8 280.4 C952.4 280.4 953.9 279.6 954.7 278.3 955.7 277 955.9 275.4 955.3 273.9 Z M199.4 204.4 C199.1 204.4 198.8 204.3 198.5 204.2 198.8 204.3 199.1 204.4 199.4 204.4 L199.6 204.4 C199.6 204.4 199.5 204.4 199.4 204.4 Z M934.4 278.6 C934.7 278.8 935 279 935.3 279.1 935 278.9 934.7 278.8 934.4 278.6 Z"
fill-opacity="1"
fill="url(#radial-gradient)"
stroke="none"
/>
</g>
</g>
</g>
</g>
</svg>
</NetworkCard>

View File

@@ -1,69 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/hbo">
<svg
width="512"
height="211"
viewBox="0 0 512 211"
version="1.1"
id="svg200"
sodipodi:docname="Hbo logo 1.svg"
inkscape:version="1.2 (dc2aedaf03, 2022-05-15)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:cc="http://creativecommons.org/ns#"
>
<defs id="defs204" />
<sodipodi:namedview
id="namedview202"
pagecolor="#ffffff"
bordercolor="#111111"
borderopacity="1"
inkscape:showpageshadow="0"
inkscape:pageopacity="0"
inkscape:pagecheckerboard="1"
inkscape:deskcolor="#d1d1d1"
showgrid="false"
inkscape:zoom="2.3543242"
inkscape:cx="129.33648"
inkscape:cy="119.7796"
inkscape:window-width="1920"
inkscape:window-height="1009"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="hbo-logo-blk"
/>
<g
id="hbo-logo-blk"
stroke="none"
stroke-width="1"
fill="none"
fill-rule="evenodd"
transform="translate(0,3.0006123e-5)"
>
<polyline
id="Fill-15"
fill="currentColor"
points="148 206 92.7748343 206 92.7748343 127.236832 56.9262346 127.236832 56.9262346 206 0 206 0 3 56.9262346 3 56.9262346 78.5141926 92.7748343 78.5141926 92.7748343 3 148 3 148 206"
transform="scale(1.0150417,1.0183117)"
/>
<path
d="m 407.03751,146.25835 c 22.74616,0 41.18655,-18.48231 41.18655,-41.28778 0,-22.799359 -18.44039,-41.284724 -41.18655,-41.284724 -22.74918,0 -41.18755,18.485365 -41.18755,41.284724 0,22.80547 18.43837,41.28778 41.18755,41.28778 z m -52.39287,-41.28778 c 0,-29.003142 23.45567,-52.51778 52.39287,-52.51778 28.93621,0 52.39593,23.514638 52.39593,52.51778 0,29.00823 -23.45972,52.52185 -52.39593,52.52185 -28.9372,0 -52.39287,-23.51362 -52.39287,-52.52185 z m -72.95472,0 c 6.48297,-0.82297 17.28995,-8.253081 21.09702,-12.995836 -1.33794,5.880691 -1.44032,22.900966 0.12367,28.781656 -4.34529,-6.7057 -14.63332,-14.9598 -21.22069,-15.78582 z M 244.0236,49.370203 c 8.23346,0 14.71745,8.147419 14.71745,17.432774 0,9.285356 -6.48399,17.437855 -14.71745,17.437855 H 215.72395 V 49.370203 Z m -0.10338,77.471097 c 8.23244,0 14.71744,8.15148 14.71744,17.43379 0,9.28637 -6.485,17.43481 -14.71744,17.43481 h -28.29966 v -34.8686 z m 163.08283,83.94921 C 464.66766,210.76815 511.5942,162.74115 511.58103,104.92994 511.56987,45.899497 464.66766,0.04371976 407.00305,5.4946177e-7 349.40535,-0.04162544 319.97554,42.458256 312.70398,59.612644 312.7719,33.860743 287.45121,3.4646403 257.83287,3.4321278 h -95.4262 V 209.76331 l 88.96958,0.0173 c 35.87021,0 61.39463,-31.15609 61.43822,-57.87219 8.18784,16.81403 36.59088,58.90243 94.18858,58.88211 z"
id="Fill-16"
fill="currentColor"
style="stroke-width:1.01668"
/>
</g>
<metadata id="metadata439">
<rdf:RDF>
<cc:Work rdf:about="" />
</rdf:RDF>
</metadata>
</svg>
</NetworkCard>

View File

@@ -1,74 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/hulu">
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="1000"
height="329.81302"
viewBox="0 0 999.99997 329.81303"
version="1.1"
id="svg87"
inkscape:version="0.92.3 (2405546, 2018-03-11)"
sodipodi:docname="Hulu.svg"
>
<defs id="defs81">
<clipPath id="clipPath20" clipPathUnits="userSpaceOnUse">
<path inkscape:connector-curvature="0" id="path18" d="M 0,0 H 1920 V 1080 H 0 Z" />
</clipPath>
</defs>
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="0.7"
inkscape:cx="1134.4996"
inkscape:cy="-132.61488"
inkscape:document-units="px"
inkscape:current-layer="layer1"
showgrid="false"
inkscape:window-width="1366"
inkscape:window-height="705"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
units="px"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
/>
<metadata id="metadata84">
<rdf:RDF>
<cc:Work rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title />
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(463.37194,-69.476276)"
>
<path
inkscape:connector-curvature="0"
d="m -241.01194,192.76828 c 16.556,13.46 24.846,34.472 24.846,63.043 v 143.478 h -78.882 v -132.919 c 0,-5.794 -2.174,-10.869 -6.521,-15.217 -4.349,-4.349 -9.425,-6.521 -15.218,-6.521 h -45.963 c -5.803,0 -10.772,2.173 -14.906,6.521 -4.144,4.348 -6.212,9.423 -6.212,15.217 v 132.919 h -79.504 V 69.476276 h 79.503 V 176.92928 c 1.243,-0.408 4.134,-1.242 8.697,-2.485 4.551,-1.241 10.557,-1.862 18.012,-1.862 h 50.931 c 26.912,-0.001 48.652,6.734 65.217,20.186 z M 9.2990586,172.58128 H 88.802059 v 139.13 c 0,26.087 -7.144,47.205 -21.428,63.354 -14.286,16.149 -33.648,24.224 -58.0750004,24.224 H -68.961941 c -27.329,0 -49.174999,-7.55 -65.526999,-22.67 -16.364,-15.111 -24.536,-36.743 -24.536,-64.907 v -139.13 h 79.502999 v 132.919 c 0,5.804 2.069,10.772 6.212,14.907 4.134,4.144 9.104,6.211 14.907,6.211 h 45.964 c 5.7929996,0 10.8679996,-2.067 15.2169996,-6.211 4.349,-4.135 6.521,-9.104 6.521,-14.907 z M 149.05006,69.476276 h 79.503 V 399.28928 h -79.503 z M 536.62804,172.58128 v 139.13 c 0,26.087 -7.144,47.205 -21.429,63.354 -14.285,16.149 -33.646,24.224 -58.074,24.224 h -77.64 c -27.746,0 -49.797,-7.55 -66.149,-22.67 -16.363,-15.111 -24.534,-36.743 -24.534,-64.907 v -139.13 h 79.503 v 132.919 c 0,5.804 2.066,10.772 6.211,14.907 4.135,4.144 9.104,6.211 14.906,6.211 h 46.584 c 5.795,0 10.764,-2.067 14.907,-6.211 4.134,-4.135 6.212,-9.104 6.212,-14.907 v -132.92 z"
id="path4"
style="fill:currentColor;fill-opacity:1"
/>
</g>
</svg>
</NetworkCard>

View File

@@ -1,12 +0,0 @@
<script>
import NetworkCard from './NetworkCard.svelte';
</script>
<NetworkCard href="/discover/netflix">
<svg xmlns="http://www.w3.org/2000/svg" width="1024" height="276.742" viewBox="0 0 1024 276.742"
><path
d="M140.803 258.904c-15.404 2.705-31.079 3.516-47.294 5.676l-49.458-144.856v151.073c-15.404 1.621-29.457 3.783-44.051 5.945v-276.742h41.08l56.212 157.021v-157.021h43.511v258.904zm85.131-157.558c16.757 0 42.431-.811 57.835-.811v43.24c-19.189 0-41.619 0-57.835.811v64.322c25.405-1.621 50.809-3.785 76.482-4.596v41.617l-119.724 9.461v-255.39h119.724v43.241h-76.482v58.105zm237.284-58.104h-44.862v198.908c-14.594 0-29.188 0-43.239.539v-199.447h-44.862v-43.242h132.965l-.002 43.242zm70.266 55.132h59.187v43.24h-59.187v98.104h-42.433v-239.718h120.808v43.241h-78.375v55.133zm148.641 103.507c24.594.539 49.456 2.434 73.51 3.783v42.701c-38.646-2.434-77.293-4.863-116.75-5.676v-242.689h43.24v201.881zm109.994 49.457c13.783.812 28.377 1.623 42.43 3.242v-254.58h-42.43v251.338zm231.881-251.338l-54.863 131.615 54.863 145.127c-16.217-2.162-32.432-5.135-48.648-7.838l-31.078-79.994-31.617 73.51c-15.678-2.705-30.812-3.516-46.484-5.678l55.672-126.75-50.269-129.992h46.482l28.377 72.699 30.27-72.699h47.295z"
fill="currentColor"
/></svg
>
</NetworkCard>

View File

@@ -1,10 +0,0 @@
<script lang="ts">
export let href: string;
</script>
<a
{href}
class="border rounded-xl h-52 w-96 bg-stone-800 border-stone-700 flex items-center justify-center cursor-pointer hover:scale-[103%] p-12 text-zinc-300 hover:text-amber-200 transition-all"
>
<slot />
</a>

View File

@@ -0,0 +1,69 @@
<script lang="ts">
import { getTmdbGenreMovies } from '$lib/apis/tmdb/tmdbApi';
import Card from '$lib/components/Card/Card.svelte';
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
import { fetchCardTmdbProps } from '$lib/components/Card/card';
import GridPage from '$lib/components/GridPage/GridPage.svelte';
import { genres, type Genre } from '$lib/discover';
import type { PageData } from './$types';
export let data: PageData;
const genre = genres[data.genre];
async function fetchGenreItems(genre: Genre) {
const movies = await getTmdbGenreMovies(genre.tmdbGenreId);
const itemProps = await Promise.all(movies.map(fetchCardTmdbProps));
return {
movies,
itemProps
};
}
</script>
<GridPage title={data.genre}>
{#if genre}
{#await fetchGenreItems(genre)}
{#each [...Array(20).keys()] as index (index)}
<CardPlaceholder size="dynamic" {index} />
{/each}
{:then { itemProps }}
{#each itemProps as itemProps}
<Card {...itemProps} size="dynamic" />
{/each}
{:catch error}
{error.message}
{/await}
{:else}
404
{/if}
</GridPage>
<!-- <div class="pt-24 p-8 bg-black">
<button on:click={() => window?.history?.back()}>Back</button>
{data.genre}
</div>
<div class="p-8">
{#if genre}
{#await fetchGenreItems(genre)}
<CardGrid>
{#each [...Array(20).keys()] as index (index)}
<CardPlaceholder size="dynamic" {index} />
{/each}
</CardGrid>
{:then { itemProps }}
<CardGrid>
{#each itemProps as itemProps}
<Card {...itemProps} size="dynamic" />
{/each}
</CardGrid>
{:catch error}
{error.message}
{/await}
{:else}
404
{/if}
</div> -->

View File

@@ -0,0 +1,7 @@
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
return {
genre: params.genre
};
}) satisfies PageLoad;

View File

@@ -0,0 +1,46 @@
<script lang="ts">
import { getTmdbNetworkSeries } from '$lib/apis/tmdb/tmdbApi';
import Card from '$lib/components/Card/Card.svelte';
import CardGrid from '$lib/components/Card/CardGrid.svelte';
import CardPlaceholder from '$lib/components/Card/CardPlaceholder.svelte';
import { fetchCardTmdbSeriesProps } from '$lib/components/Card/card';
import type { ComponentProps } from 'svelte';
import type { PageData } from './$types';
import GridPage from '$lib/components/GridPage/GridPage.svelte';
import { networks, type Network } from '../../../../lib/discover';
export let data: PageData;
const network = networks[data.network] || undefined;
async function fetchNetworkSeries(network: Network) {
const shows = await getTmdbNetworkSeries(network.tmdbNetworkId);
const showProps: ComponentProps<Card>[] = await Promise.all(
shows.map(fetchCardTmdbSeriesProps)
);
return {
shows,
showProps
};
}
</script>
<GridPage title={data.network}>
{#if network}
{#await fetchNetworkSeries(network)}
{#each [...Array(20).keys()] as index (index)}
<CardPlaceholder size="dynamic" {index} />
{/each}
{:then { showProps }}
{#each showProps as showProps}
<Card {...showProps} size="dynamic" />
{/each}
{:catch error}
{error.message}
{/await}
{:else}
404
{/if}
</GridPage>

View File

@@ -0,0 +1,7 @@
import type { PageLoad } from './$types';
export const load = (async ({ params }) => {
return {
network: params.network
};
}) satisfies PageLoad;

View File

@@ -1,28 +0,0 @@
import type { PageServerLoad } from './$types';
import { RadarrApi, getRadarrMovies } from '$lib/apis/radarr/radarrApi';
import type { CardProps } from '$lib/components/Card/card';
import { fetchCardProps } from '$lib/components/Card/card';
// interface DownloadingCardProps extends CardProps {
// progress: number;
// completionTime: string;
// }
// export const load = (() => {
// const [downloading, available, unavailable] = getLibraryItems();
// // radarrMovies.then((d) => console.log(d.map((m) => m.ratings)));
// const libraryInfo = getLibraryInfo();
// return {
// streamed: {
// libraryInfo,
// downloading,
// available,
// unavailable
// }
// };
// }) satisfies PageServerLoad;
// async function getLibraryInfo(): Promise<any> {}

View File

@@ -8,26 +8,79 @@
import { ChevronDown, MagnifyingGlass, TextAlignBottom, Trash } from 'radix-icons-svelte';
import type { ComponentProps } from 'svelte';
const posterGridStyle =
'grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5';
const headerStyle = 'uppercase tracking-widest font-bold';
const headerContaienr = 'flex items-center justify-between mt-2';
let itemsVisible: 'all' | 'movies' | 'shows' = 'all';
let sortBy: 'added' | 'rating' | 'year' | 'size' | 'name' = 'added';
let sortBy: 'added' | 'rating' | 'release' | 'size' | 'name' = 'added';
let loading = true;
let searchInput: HTMLInputElement | undefined;
let searchInputValue = '';
let items: PlayableItem[] = [];
let downloadingProps: ComponentProps<Card>[] = [];
let availableProps: ComponentProps<Card>[] = [];
let watchedProps: ComponentProps<Card>[] = [];
let unavailableProps: ComponentProps<Card>[] = [];
$: {
if (items.length) updateComponentProps(searchInputValue);
}
library.subscribe(async (libraryPromise) => {
const libraryData = await libraryPromise;
items = libraryData.itemsArray;
loading = false;
});
const items: PlayableItem[] = filterItems(sortItems(libraryData.itemsArray));
function updateComponentProps(searchInputValue: string) {
const filteredItems = items
.sort((a, b) => {
switch (sortBy) {
case 'added':
return (b.radarrMovie?.added || b.sonarrSeries?.added || '') <
(a.radarrMovie?.added || a.sonarrSeries?.added || '')
? -1
: 1;
case 'rating':
return (b.tmdbRating || 0) - (a.tmdbRating || 0);
case 'release':
return (b.radarrMovie?.inCinemas || b.sonarrSeries?.firstAired || '') <
(a.radarrMovie?.inCinemas || a.sonarrSeries?.firstAired || '')
? -1
: 1;
case 'size':
return (
(b.radarrMovie?.sizeOnDisk || b.sonarrSeries?.statistics?.sizeOnDisk || 0) -
(a.radarrMovie?.sizeOnDisk || a.sonarrSeries?.statistics?.sizeOnDisk || 0)
);
case 'name':
return (b.radarrMovie?.title?.toLowerCase() ||
b.sonarrSeries?.title?.toLowerCase() ||
'') >
(a.radarrMovie?.title?.toLowerCase() || a.sonarrSeries?.title?.toLowerCase() || '')
? -1
: 1;
}
for (let item of items) {
return 0;
})
.filter((item) => {
if (searchInputValue) {
return (
item.radarrMovie?.title?.toLowerCase().includes(searchInputValue.toLowerCase()) ||
item.sonarrSeries?.title?.toLowerCase().includes(searchInputValue.toLowerCase())
);
} else {
return true;
}
});
downloadingProps = [];
availableProps = [];
watchedProps = [];
unavailableProps = [];
for (let item of filteredItems) {
let props: ComponentProps<Card>;
const series = item.sonarrSeries;
@@ -40,7 +93,7 @@
tmdbId: String(item.tmdbId),
title: series.title || '',
genres: series.genres || [],
backdropUrl: item.cardBackdropUrl,
backdropUri: item.cardBackdropUrl,
rating: series.ratings?.value || series.ratings?.value || item.tmdbRating || 0,
seasons: series.seasons?.length || 0
};
@@ -51,7 +104,7 @@
tmdbId: String(item.tmdbId),
title: movie.title || '',
genres: movie.genres || [],
backdropUrl: item.cardBackdropUrl,
backdropUri: item.cardBackdropUrl,
rating: movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0,
runtimeMinutes: movie.runtime || 0
};
@@ -81,19 +134,23 @@
availableProps = availableProps;
watchedProps = watchedProps;
unavailableProps = unavailableProps;
loading = false;
});
function sortItems(arr: any[]) {
return arr.sort((a, b) => ((a.added || '') > (b.added || '') ? -1 : 1));
}
function filterItems(arr: any[]) {
return arr;
function handleShortcuts(event: KeyboardEvent) {
if (event.key === 'f' && (event.metaKey || event.ctrlKey)) {
event.preventDefault();
searchInput?.focus();
}
}
const posterGridStyle =
'grid gap-x-4 gap-y-8 grid-cols-2 md:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5';
const headerStyle = 'uppercase tracking-widest font-bold';
const headerContaienr = 'flex items-center justify-between mt-2';
</script>
<svelte:window on:keydown={handleShortcuts} />
<div class="pt-24 pb-8 px-8 bg-black">
<div class="max-w-screen-2xl mx-auto">
<div class="grid grid-cols-1 lg:grid-cols-2 items-center justify-center gap-4">
@@ -114,6 +171,8 @@
type="text"
class="bg-transparent outline-none text-zinc-300"
placeholder="Search from library"
bind:this={searchInput}
bind:value={searchInputValue}
/>
</div>
<div class="flex items-center gap-2 bg-stone-950 rounded-2xl p-3 px-5 shadow-lg">