diff --git a/README.md b/README.md index 505ae71..99aacc4 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,14 @@ TODO: -- [x] Jellyfin video sync +- [ ] Season Group Downloads - [ ] Mass edit local files & show space left - [ ] Finish discover page - [ ] Onboarding setup & sources - [ ] Settings page -- [ ] Sonarr support - [ ] Event notifications & show indexer status -- [ ] Plex video sync +- [ ] Library filters +- [ ] People Pages FIX: @@ -22,3 +22,8 @@ Further ideas - [ ] Similar movies & shows, actor pages and recommendations - [ ] Watchlist management - [ ] Download a movie file +- [ ] Show & write reviews +- [ ] TMDB Watched list +- [ ] People & Actors page +- [ ] Similar movies & shows recommendations +- [ ] Plex video sync diff --git a/src/lib/apis/sonarr/sonarrApi.ts b/src/lib/apis/sonarr/sonarrApi.ts index 59b02cb..b73860f 100644 --- a/src/lib/apis/sonarr/sonarrApi.ts +++ b/src/lib/apis/sonarr/sonarrApi.ts @@ -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 => { return SonarrApi.get('/api/v3/episode', { diff --git a/src/lib/apis/tmdb/tmdbApi.ts b/src/lib/apis/tmdb/tmdbApi.ts index 7e66290..1570d0a 100644 --- a/src/lib/apis/tmdb/tmdbApi.ts +++ b/src/lib/apis/tmdb/tmdbApi.ts @@ -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({ @@ -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 => +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 => 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 => +export const getTmdbSeries = async (tmdbId: number): Promise => await TmdbApiOpen.get('/3/tv/{series_id}', { params: { path: { @@ -73,12 +74,12 @@ export const getTmdbSeries = async (tmdbId: number): Promise res.data as SeriesDetailsFull | undefined); + }).then((res) => res.data as TmdbSeriesFull2 | undefined); export const getTmdbSeriesSeason = async ( tmdbId: number, season: number -): Promise => +): Promise => 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 => await TmdbApi.get('/movie/' + tmdbId).then((r) => r.data); +/** @deprecated */ export const fetchTmdbMovieVideos = async (tmdbId: string): Promise => await TmdbApi.get('/movie/' + tmdbId + '/videos').then((res) => res.data.results); +/** @deprecated */ export const fetchTmdbMovieImages = async (tmdbId: string): Promise => await TmdbApi.get('/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 => await TmdbApi.get('/movie/' + tmdbId + '/credits').then((res) => res.data.cast); -export const fetchTmdbPopularMovies = () => - TmdbApi.get('/movie/popular').then((res) => res.data.results); - -export const requestTmdbPopularMovies = () => request(fetchTmdbPopularMovies, null); - export interface TmdbMovieFull extends TmdbMovie { videos: { results: Video[]; diff --git a/src/lib/components/Card/Card.svelte b/src/lib/components/Card/Card.svelte index b5de86a..c6ac672 100644 --- a/src/lib/components/Card/Card.svelte +++ b/src/lib/components/Card/Card.svelte @@ -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 @@ } - -
-
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 @@
-
+ diff --git a/src/lib/components/Card/CardGrid.svelte b/src/lib/components/Card/CardGrid.svelte new file mode 100644 index 0000000..f1c1e9d --- /dev/null +++ b/src/lib/components/Card/CardGrid.svelte @@ -0,0 +1,3 @@ +
+ +
diff --git a/src/lib/components/Card/card.ts b/src/lib/components/Card/card.ts index dc3fa74..a02c7d7 100644 --- a/src/lib/components/Card/card.ts +++ b/src/lib/components/Card/card.ts @@ -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 => { - 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> => { + 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 => { - 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> => { + 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> => { + if ('name' in item) return fetchCardTmdbSeriesProps(item); + return fetchCardTmdbMovieProps(item); +}; diff --git a/src/lib/components/Carousel/Carousel.svelte b/src/lib/components/Carousel/Carousel.svelte index 1dc8021..c377efe 100644 --- a/src/lib/components/Carousel/Carousel.svelte +++ b/src/lib/components/Carousel/Carousel.svelte @@ -14,14 +14,14 @@
{ - carousel?.scrollTo({ left: scrollX - carousel?.clientWidth, behavior: 'smooth' }); + carousel?.scrollTo({ left: scrollX - carousel?.clientWidth * 0.8, behavior: 'smooth' }); }} > { - carousel?.scrollTo({ left: scrollX + carousel?.clientWidth, behavior: 'smooth' }); + carousel?.scrollTo({ left: scrollX + carousel?.clientWidth * 0.8, behavior: 'smooth' }); }} > diff --git a/src/lib/components/GenreCard.svelte b/src/lib/components/GenreCard.svelte new file mode 100644 index 0000000..e0aa74b --- /dev/null +++ b/src/lib/components/GenreCard.svelte @@ -0,0 +1,27 @@ + + + + + + +
+

+ {capitalize(genre.name)} +

+
+
+
+ diff --git a/src/lib/components/GridPage/GridPage.svelte b/src/lib/components/GridPage/GridPage.svelte new file mode 100644 index 0000000..c5b0601 --- /dev/null +++ b/src/lib/components/GridPage/GridPage.svelte @@ -0,0 +1,34 @@ + + +
+ +
+ +

{capitalize(title)}

+
window?.history?.back()} + > + +

Back

+
+
+
+ +
+ + + {#each [...Array(20).keys()] as index (index)} + + {/each} + + +
diff --git a/src/lib/components/NetworkCard.svelte b/src/lib/components/NetworkCard.svelte new file mode 100644 index 0000000..168a58d --- /dev/null +++ b/src/lib/components/NetworkCard.svelte @@ -0,0 +1,17 @@ + + + +
+ diff --git a/src/lib/components/PeopleCard/PeopleCard.svelte b/src/lib/components/PeopleCard/PeopleCard.svelte new file mode 100644 index 0000000..bbc77b2 --- /dev/null +++ b/src/lib/components/PeopleCard/PeopleCard.svelte @@ -0,0 +1,45 @@ + + + +
+
+ +
+
+

+ {department} +

+

{name}

+
+
+
+ diff --git a/src/lib/discover.ts b/src/lib/discover.ts new file mode 100644 index 0000000..b40c7b4 --- /dev/null +++ b/src/lib/discover.ts @@ -0,0 +1,115 @@ +export interface Network { + name: string; + tmdbNetworkId: number; +} + +export const networks: Record = { + 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 = { + 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 + } +}; diff --git a/src/lib/stores/library.store.ts b/src/lib/stores/library.store.ts index 153400a..4b5fcca 100644 --- a/src/lib/stores/library.store.ts +++ b/src/lib/stores/library.store.ts @@ -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 { ? { 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 { : 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 { function createLibraryStore() { const { update, set, ...library } = writable>(getLibrary()); //TODO promise to undefined + async function filterNotInLibrary(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 }; } diff --git a/src/lib/stores/settings.store.ts b/src/lib/stores/settings.store.ts index b369880..09fa29f 100644 --- a/src/lib/stores/settings.store.ts +++ b/src/lib/stores/settings.store.ts @@ -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(defaultSettings); + +export const getIncludedLanguagesQuery = () => { + const settingsValue = get(settings); + if (settingsValue.discover.filterBasedOnLanguage) { + return { with_original_language: settingsValue.language }; + } + + return {}; +}; diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 83b3066..4a21992 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -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(' '); +} diff --git a/src/routes/+page.server.ts b/src/routes/+page.server.ts index ceb41fa..62f3b4c 100644 --- a/src/routes/+page.server.ts +++ b/src/routes/+page.server.ts @@ -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 () => { diff --git a/src/routes/discover/+page.server.ts b/src/routes/discover/+page.server.ts deleted file mode 100644 index fc36a45..0000000 --- a/src/routes/discover/+page.server.ts +++ /dev/null @@ -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; diff --git a/src/routes/discover/+page.svelte b/src/routes/discover/+page.svelte index d409865..a3abe8a 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -1,80 +1,166 @@
- -
-
For You
- {#await discoverPromise} +
Trending
+ {#await fetchTrendingProps()} - {:then { popularMovies: movies }} - {#each movies ? [...movies].reverse() : [] as movie (movie.tmdbId)} - + {:then props} + {#each props as prop} + {/each} {/await}
-
Popular Movies
- {#await discoverPromise} +
Popular People
+ {#await fetchTrendingActors()} - {:then { popularMovies: movies }} - {#each movies || [] as movie (movie.tmdbId)} - + {:then props} + {#each props as prop} + {/each} {/await}
-
-
Networks
- - - - - - +
Upcoming Movies
+ {#await fetchUpcomingMovies()} + + {:then props} + {#each props as prop} + + {/each} + {/await} +
+
+
+ +
Upcoming Series
+ {#await fetchUpcomingSeries()} + + {:then props} + {#each props as prop} + + {/each} + {/await} +
+
+
+ +
Genres
+ {#each Object.values(genres) as genre} + + {/each} +
+
+
+ +
New Digital Releeases
+ {#await fetchDigitalReleases()} + + {:then props} + {#each props as prop} + + {/each} + {/await} +
+
+
+ +
Streaming Now
+ {#await fetchNowStreaming()} + + {:then props} + {#each props as prop} + + {/each} + {/await} +
+
+
+ +
TV Networks
+ {#each Object.values(networks) as network} + + {/each}
-
diff --git a/src/routes/discover/AmazonCard.svelte b/src/routes/discover/AmazonCard.svelte deleted file mode 100644 index a3beb8a..0000000 --- a/src/routes/discover/AmazonCard.svelte +++ /dev/null @@ -1,55 +0,0 @@ - - - - - - PrimeLogo_Blue - - - - - - - - - diff --git a/src/routes/discover/NetworkCard.svelte b/src/routes/discover/NetworkCard.svelte deleted file mode 100644 index f04f817..0000000 --- a/src/routes/discover/NetworkCard.svelte +++ /dev/null @@ -1,10 +0,0 @@ - - - - - diff --git a/src/routes/discover/genre/[genre]/+page.svelte b/src/routes/discover/genre/[genre]/+page.svelte new file mode 100644 index 0000000..ad756f9 --- /dev/null +++ b/src/routes/discover/genre/[genre]/+page.svelte @@ -0,0 +1,69 @@ + + + + {#if genre} + {#await fetchGenreItems(genre)} + {#each [...Array(20).keys()] as index (index)} + + {/each} + {:then { itemProps }} + {#each itemProps as itemProps} + + {/each} + {:catch error} + {error.message} + {/await} + {:else} + 404 + {/if} + + + diff --git a/src/routes/discover/genre/[genre]/+page.ts b/src/routes/discover/genre/[genre]/+page.ts new file mode 100644 index 0000000..a1327a5 --- /dev/null +++ b/src/routes/discover/genre/[genre]/+page.ts @@ -0,0 +1,7 @@ +import type { PageLoad } from './$types'; + +export const load = (async ({ params }) => { + return { + genre: params.genre + }; +}) satisfies PageLoad; diff --git a/src/routes/discover/network/[network]/+page.svelte b/src/routes/discover/network/[network]/+page.svelte new file mode 100644 index 0000000..aae284f --- /dev/null +++ b/src/routes/discover/network/[network]/+page.svelte @@ -0,0 +1,46 @@ + + + + {#if network} + {#await fetchNetworkSeries(network)} + {#each [...Array(20).keys()] as index (index)} + + {/each} + {:then { showProps }} + {#each showProps as showProps} + + {/each} + {:catch error} + {error.message} + {/await} + {:else} + 404 + {/if} + diff --git a/src/routes/discover/network/[network]/+page.ts b/src/routes/discover/network/[network]/+page.ts new file mode 100644 index 0000000..2a2fd5b --- /dev/null +++ b/src/routes/discover/network/[network]/+page.ts @@ -0,0 +1,7 @@ +import type { PageLoad } from './$types'; + +export const load = (async ({ params }) => { + return { + network: params.network + }; +}) satisfies PageLoad; diff --git a/src/routes/library/+page.server.ts b/src/routes/library/+page.server.ts deleted file mode 100644 index 2e485d5..0000000 --- a/src/routes/library/+page.server.ts +++ /dev/null @@ -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 {} diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index 95c9c96..c30c92a 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -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[] = []; let availableProps: ComponentProps[] = []; let watchedProps: ComponentProps[] = []; let unavailableProps: ComponentProps[] = []; + $: { + 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; 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'; + +
@@ -114,6 +171,8 @@ type="text" class="bg-transparent outline-none text-zinc-300" placeholder="Search from library" + bind:this={searchInput} + bind:value={searchInputValue} />
diff --git a/static/genres/action.jpg b/static/genres/action.jpg new file mode 100644 index 0000000..3598140 Binary files /dev/null and b/static/genres/action.jpg differ diff --git a/static/genres/adventure.jpg b/static/genres/adventure.jpg new file mode 100644 index 0000000..012913f Binary files /dev/null and b/static/genres/adventure.jpg differ diff --git a/static/genres/animation.jpg b/static/genres/animation.jpg new file mode 100644 index 0000000..62fa6bc Binary files /dev/null and b/static/genres/animation.jpg differ diff --git a/static/genres/comedy.jpg b/static/genres/comedy.jpg new file mode 100644 index 0000000..1d7e062 Binary files /dev/null and b/static/genres/comedy.jpg differ diff --git a/static/genres/crime.jpg b/static/genres/crime.jpg new file mode 100644 index 0000000..e707a85 Binary files /dev/null and b/static/genres/crime.jpg differ diff --git a/static/networks/amazon.svg b/static/networks/amazon.svg new file mode 100644 index 0000000..e031863 --- /dev/null +++ b/static/networks/amazon.svg @@ -0,0 +1,13 @@ + + +PrimeLogo_Blue + + + + + + + + \ No newline at end of file diff --git a/src/routes/discover/AppleCard.svelte b/static/networks/apple.svg similarity index 77% rename from src/routes/discover/AppleCard.svelte rename to static/networks/apple.svg index 386ba5a..e7b8800 100644 --- a/src/routes/discover/AppleCard.svelte +++ b/static/networks/apple.svg @@ -1,11 +1,5 @@ - - - - - + > \ No newline at end of file diff --git a/src/routes/discover/DisneyCard.svelte b/static/networks/disney.svg similarity index 98% rename from src/routes/discover/DisneyCard.svelte rename to static/networks/disney.svg index 1ec800f..34db5c0 100644 --- a/src/routes/discover/DisneyCard.svelte +++ b/static/networks/disney.svg @@ -1,9 +1,4 @@ - - - - - - + \ No newline at end of file diff --git a/src/routes/discover/HboCard.svelte b/static/networks/hbo.svg similarity index 92% rename from src/routes/discover/HboCard.svelte rename to static/networks/hbo.svg index 1faefa9..e6747b3 100644 --- a/src/routes/discover/HboCard.svelte +++ b/static/networks/hbo.svg @@ -1,9 +1,4 @@ - - - - - - - - - - - + \ No newline at end of file diff --git a/src/routes/discover/HuluCard.svelte b/static/networks/hulu.svg similarity index 95% rename from src/routes/discover/HuluCard.svelte rename to static/networks/hulu.svg index 6cb17a6..8ebe2de 100644 --- a/src/routes/discover/HuluCard.svelte +++ b/static/networks/hulu.svg @@ -1,9 +1,4 @@ - - - - - - + \ No newline at end of file diff --git a/src/routes/discover/NetflixCard.svelte b/static/networks/netflix.svg similarity index 82% rename from src/routes/discover/NetflixCard.svelte rename to static/networks/netflix.svg index 91dbf79..887872e 100644 --- a/src/routes/discover/NetflixCard.svelte +++ b/static/networks/netflix.svg @@ -1,12 +1,6 @@ - - - - - + > \ No newline at end of file