diff --git a/README.md b/README.md index 5c91169..83519e4 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,14 @@ -# create-svelte +# Reiverr -Everything you need to build a Svelte project, powered by [`create-svelte`](https://github.com/sveltejs/kit/tree/master/packages/create-svelte). +TODO: +- [ ] Sonarr support +- [ ] Onboarding setup & sources +- [ ] Settings page +- [ ] Plex and Jellyfin sync +- [ ] Mass edit local files & show space left +- [ ] Finish discover page +- [ ] Event notifications & show indexer status -## Creating a project - -If you're seeing this, you've probably already done this step. Congrats! - -```bash -# create a new project in the current directory -npm create svelte@latest - -# create a new project in my-app -npm create svelte@latest my-app -``` - -## Developing - -Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a development server: - -```bash -npm run dev - -# or start the server and open the app in a new browser tab -npm run dev -- --open -``` - -## Building - -To create a production version of your app: - -```bash -npm run build -``` - -You can preview the production build with `npm run preview`. - -> To deploy your app, you may need to install an [adapter](https://kit.svelte.dev/docs/adapters) for your target environment. +Further ideas +- [ ] Similar movies & shows, actor pages and recommendations +- [ ] Watchlist management \ No newline at end of file diff --git a/src/lib/radarr/radarr.ts b/src/lib/radarr/radarr.ts index 11ed58f..d78f03f 100644 --- a/src/lib/radarr/radarr.ts +++ b/src/lib/radarr/radarr.ts @@ -5,7 +5,7 @@ import type { components } from '$lib/radarr/radarr-types'; import { fetchTmdbMovie } from '$lib/tmdb-api'; import { RADARR_API_KEY, RADARR_BASE_URL } from '$env/static/private'; -export type MovieResource = components['schemas']['MovieResource']; +export type RadarrMovie = components['schemas']['MovieResource']; export type MovieFileResource = components['schemas']['MovieFileResource']; export type ReleaseResource = components['schemas']['ReleaseResource']; @@ -134,4 +134,4 @@ const getMovieByTmdbIdByTmdbId = (tmdbId: string) => tmdbId: Number(tmdbId) } } - }).then((r) => r.data as any as MovieResource); + }).then((r) => r.data as any as RadarrMovie); diff --git a/src/routes/components/Card/Card.svelte b/src/routes/components/Card/Card.svelte index 6682f12..c60ec56 100644 --- a/src/routes/components/Card/Card.svelte +++ b/src/routes/components/Card/Card.svelte @@ -1,13 +1,20 @@ -{#if !tmdbMovie || !backdropUrl} - -{:else} +
+
window.open('/movie/' + 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;' : ''} > -
-
window.open('/movie/' + tmdbMovie.id, '_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;' : ''} - > -
-

{tmdbMovie.original_title}

-
- {formatGenres(tmdbMovie.genres)} -
-
-
- {#if progressType === 'watched'} -
- {progress - ? formatMinutesToTime(tmdbMovie.runtime - tmdbMovie.runtime * (progress / 100)) + - ' left' - : formatMinutesToTime(tmdbMovie.runtime)} -
- {:else if progressType === 'downloading'} -
- {Math.floor(progress) + '% Downloaded'} -
- {/if} +
+

{title}

+
+ {genres.map((genre) => genre.charAt(0).toUpperCase() + genre.slice(1)).join(', ')}
-
-
+
+ {#if completionTime} +
+ Downloaded in {formatMinutesToTime((new Date(completionTime).getTime() - Date.now()) / 1000 / 60)} +
+ {:else} + {#if runtimeMinutes} +
+ +
+ {progress + ? formatMinutesToTime(runtimeMinutes - runtimeMinutes * (progress / 100)) + ' left' + : formatMinutesToTime(runtimeMinutes)} +
+
+ {/if} + + {#if rating} +
+ +
+ {rating.toFixed(1)} +
+
+ {/if} + {/if} +
-{/if} +
+
+
diff --git a/src/routes/components/Card/CardProvider.svelte b/src/routes/components/Card/CardProvider.svelte new file mode 100644 index 0000000..21a380b --- /dev/null +++ b/src/routes/components/Card/CardProvider.svelte @@ -0,0 +1,38 @@ + + +{#await Promise.all([tmdbMoviePromise, jellyfinItemPromise, backdropUrlPromise])} + +{:then [tmdbMovie, jellyfinItem, backdropUrl]} + +{:catch err} + Error +{/await} diff --git a/src/routes/components/Card/card.ts b/src/routes/components/Card/card.ts new file mode 100644 index 0000000..c88bf91 --- /dev/null +++ b/src/routes/components/Card/card.ts @@ -0,0 +1,42 @@ +import type { RadarrMovie } from '$lib/radarr/radarr'; +import { fetchTmdbMovieImages } from '$lib/tmdb-api'; +import type { TmdbMovie } from '$lib/tmdb-api'; + +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 + ); + + 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), + runtimeMinutes: movie.runtime, + backdropUrl: (await backdropUrl) || '', + rating: movie.vote_average || 0 + }; +}; diff --git a/src/routes/discover/+page.server.ts b/src/routes/discover/+page.server.ts new file mode 100644 index 0000000..2b1a87e --- /dev/null +++ b/src/routes/discover/+page.server.ts @@ -0,0 +1,19 @@ +import type { PageServerLoad } from './$types'; +import { fetchTmdbMovie, fetchTmdbPopularMovies } from '$lib/tmdb-api'; +import { fetchCardPropsTmdb } from '../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 414d8ff..823666e 100644 --- a/src/routes/discover/+page.svelte +++ b/src/routes/discover/+page.svelte @@ -1,5 +1,4 @@
@@ -25,11 +20,11 @@
For You
- {#await popularMovies} + {#await data.streamed.popularMovies} {:then movies} - {#each movies ? [...movies].reverse() : [] as movie (movie.id)} - + {#each movies ? [...movies].reverse() : [] as movie (movie.tmdbId)} + {/each} {/await}
@@ -37,11 +32,11 @@
Popular Movies
- {#await popularMovies} + {#await data.streamed.popularMovies} {:then movies} - {#each movies || [] as movie (movie.id)} - + {#each movies || [] as movie (movie.tmdbId)} + {/each} {/await}
diff --git a/src/routes/library/+page.server.ts b/src/routes/library/+page.server.ts index 636db49..0250887 100644 --- a/src/routes/library/+page.server.ts +++ b/src/routes/library/+page.server.ts @@ -1,5 +1,7 @@ import type { PageServerLoad } from './$types'; import { RadarrApi } from '$lib/radarr/radarr'; +import type { CardProps } from '../components/Card/card'; +import { fetchCardProps } from '../components/Card/card'; export const load = (() => { const radarrMovies = RadarrApi.get('/api/v3/movie', { @@ -14,42 +16,60 @@ export const load = (() => { } }).then((r) => r.data?.records?.filter((record) => record.movie)); - const downloading = downloadingRadarrMovies.then(async (movies) => { - return movies?.map((m) => ({ - tmdbId: m.movie?.tmdbId, - size: m.size, - sizeleft: m.sizeleft - })); - }); - - const unavailable = radarrMovies.then(async (movies) => { - const downloadingMovies = await downloading; - return movies?.filter( - (m) => - (!m.movieFile || !m.hasFile || !m.isAvailable) && - !downloadingMovies?.find((d) => d.tmdbId === m.tmdbId) + const unavailable: Promise = radarrMovies.then(async (movies) => { + const downloadingMovies = await downloadingRadarrMovies; + return await Promise.all( + movies + ?.filter( + (m) => + (!m.movieFile || !m.movieFile || !m.isAvailable) && + !downloadingMovies?.find((d) => d.movie?.tmdbId === m.tmdbId) + ) + .map(async (m) => fetchCardProps(m)) || [] ); }); - const available = radarrMovies.then(async (movies) => { + const available: Promise = radarrMovies.then(async (movies) => { const downloadingMovies = await downloading; const unavailableMovies = await unavailable; if (!downloadingMovies || !movies) return []; - return movies - .filter((movie) => { - return !downloadingMovies.find((downloadingMovie) => downloadingMovie.tmdbId === movie.id); - }) - .filter( - (movie) => !unavailableMovies?.find((unavailableMovie) => unavailableMovie.id === movie.id) - ); + return await Promise.all( + movies + .filter((movie) => { + return !downloadingMovies.find( + (downloadingMovie) => downloadingMovie.tmdbId === String(movie.tmdbId) + ); + }) + .filter( + (movie) => + !unavailableMovies?.find( + (unavailableMovie) => unavailableMovie.tmdbId === String(movie.tmdbId) + ) + ) + .map(async (m) => fetchCardProps(m)) || [] + ); }); + const downloading: Promise = downloadingRadarrMovies.then(async (movies) => { + return Promise.all( + movies + ?.filter((m) => m?.movie?.tmdbId) + ?.map(async (m) => ({ + ...(await fetchCardProps(m.movie as any)), + progress: m.sizeleft && m.size ? ((m.size - m.sizeleft) / m.size) * 100 : 0, + completionTime: m.estimatedCompletionTime + })) || [] + ); + }); + + // radarrMovies.then((d) => console.log(d.map((m) => m.ratings))); + return { streamed: { - available, downloading, + available, unavailable } }; diff --git a/src/routes/library/+page.svelte b/src/routes/library/+page.svelte index 30f16b9..98b9a70 100644 --- a/src/routes/library/+page.svelte +++ b/src/routes/library/+page.svelte @@ -1,10 +1,9 @@