Updated movie page
This commit is contained in:
@@ -260,6 +260,24 @@ export const getTmdbSeriesCredits = (tmdbId: number) =>
|
||||
}
|
||||
}).then((res) => res.data?.cast || []);
|
||||
|
||||
export const getTmdbMovieRecommendations = (tmdbId: number) =>
|
||||
TmdbApiOpen.get('/3/movie/{movie_id}/recommendations', {
|
||||
params: {
|
||||
path: {
|
||||
movie_id: tmdbId
|
||||
}
|
||||
}
|
||||
}).then((res) => res.data?.results || []);
|
||||
|
||||
export const getTmdbMovieSimilar = (tmdbId: number) =>
|
||||
TmdbApiOpen.get('/3/movie/{movie_id}/similar', {
|
||||
params: {
|
||||
path: {
|
||||
movie_id: tmdbId
|
||||
}
|
||||
}
|
||||
}).then((res) => res.data?.results || []);
|
||||
|
||||
// Deprecated hereon forward
|
||||
|
||||
/** @deprecated */
|
||||
|
||||
127
src/lib/components/DetailsPage/DetailsPage.svelte
Normal file
127
src/lib/components/DetailsPage/DetailsPage.svelte
Normal file
@@ -0,0 +1,127 @@
|
||||
<script lang="ts">
|
||||
import { TMDB_IMAGES_ORIGINAL, TMDB_POSTER_SMALL } from '$lib/constants';
|
||||
import { DotFilled } from 'radix-icons-svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import Carousel from '../Carousel/Carousel.svelte';
|
||||
import CarouselPlaceholderItems from '../Carousel/CarouselPlaceholderItems.svelte';
|
||||
|
||||
export let backdropPath: string;
|
||||
export let posterPath: string;
|
||||
export let title: string;
|
||||
|
||||
export let tagline: string;
|
||||
export let overview: string;
|
||||
</script>
|
||||
|
||||
<div class="flex flex-col max-h-screen bg-black pb-2" transition:fade>
|
||||
<div
|
||||
style={"background-image: url('" + TMDB_IMAGES_ORIGINAL + backdropPath + "')"}
|
||||
class="flex-shrink relative flex pt-24 aspect-video min-h-[70vh] p-4 sm:p-8 bg-center bg-cover w-screen sm:bg-fixed"
|
||||
>
|
||||
<div class="absolute inset-0 bg-gradient-to-t from-black to-50% to-darken" />
|
||||
<div class="z-[1] flex-1 flex justify-end gap-8 items-end">
|
||||
<div
|
||||
class="aspect-[2/3] w-52 bg-center bg-cover rounded-md hidden sm:block"
|
||||
style={"background-image: url('" + TMDB_POSTER_SMALL + posterPath + "')"}
|
||||
/>
|
||||
<div class="flex-1 flex gap-4 justify-between flex-col lg:flex-row lg:items-end">
|
||||
<div>
|
||||
<div class="text-zinc-300 text-sm uppercase font-semibold flex items-center gap-1">
|
||||
<p class="flex-shrink-0">
|
||||
<slot name="title-info-1" />
|
||||
</p>
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0">
|
||||
<slot name="title-info-2" />
|
||||
</p>
|
||||
<DotFilled />
|
||||
<p class="flex-shrink-0"><slot name="title-info-3" /></p>
|
||||
<!-- <DotFilled />
|
||||
<p class="line-clamp-1">{series?.genres?.map((g) => g.name).join(', ')}</p> -->
|
||||
</div>
|
||||
<h1 class="text-4xl sm:text-5xl md:text-6xl font-semibold">{title}</h1>
|
||||
</div>
|
||||
<div class="flex-shrink-0">
|
||||
<slot name="title-right" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<slot name="episodes-carousel" />
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col py-4 gap-8 bg-black">
|
||||
<div
|
||||
class="mx-4 sm:mx-8 py-4 sm:py-6 px-6 sm:px-10 grid grid-cols-4 sm:grid-cols-6 gap-4 sm:gap-x-8 bg-zinc-900 rounded-xl"
|
||||
>
|
||||
<slot name="info-description">
|
||||
<div
|
||||
class="flex flex-col gap-3 max-w-5xl row-span-3 col-span-4 sm:col-span-6 lg:col-span-3 mb-4 lg:mr-8"
|
||||
>
|
||||
<div class="flex gap-4 justify-between">
|
||||
<h1 class="font-semibold text-xl sm:text-2xl">{tagline}</h1>
|
||||
<!-- <div class="flex items-center gap-4">
|
||||
<a
|
||||
target="_blank"
|
||||
href={'https://www.themoviedb.org/tv/' + tmdbId}
|
||||
class="opacity-60 hover:opacity-100"
|
||||
>
|
||||
<img src="/tmdb.svg" alt="tmdb" width="25px" />
|
||||
</a>
|
||||
{#if $itemStore.item?.sonarrSeries?.titleSlug}
|
||||
<a
|
||||
target="_blank"
|
||||
href={PUBLIC_SONARR_BASE_URL +
|
||||
'/series/' +
|
||||
$itemStore.item?.sonarrSeries?.titleSlug}
|
||||
class="opacity-60 hover:opacity-100"
|
||||
>
|
||||
<img src="/sonarr.svg" alt="sonarr" width="15px" />
|
||||
</a>
|
||||
{/if}
|
||||
{#if series?.homepage}
|
||||
<a
|
||||
target="_blank"
|
||||
href={series.homepage}
|
||||
class="flex gap-1 items-center opacity-60 hover:opacity-100"
|
||||
>
|
||||
<Globe size={15} />
|
||||
</a>
|
||||
{/if} -->
|
||||
</div>
|
||||
<p class="pl-4 border-l-2 text-sm sm:text-base">{overview}</p>
|
||||
</div>
|
||||
</slot>
|
||||
<slot name="info-components" />
|
||||
<slot name="servarr-components">
|
||||
<div class="flex gap-4 flex-wrap col-span-4 sm:col-span-6 mt-4">
|
||||
<div class="placeholder h-10 w-40 rounded-xl" />
|
||||
<div class="placeholder h-10 w-40 rounded-xl" />
|
||||
</div>
|
||||
</slot>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel gradientFromColor="from-black">
|
||||
<slot name="cast-crew-carousel-title" slot="title" />
|
||||
<slot name="cast-crew-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel gradientFromColor="from-black">
|
||||
<slot name="recommendations-carousel-title" slot="title" />
|
||||
<slot name="recommendations-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
<div>
|
||||
<Carousel gradientFromColor="from-black">
|
||||
<slot name="similar-carousel-title" slot="title" />
|
||||
<slot name="similar-carousel">
|
||||
<CarouselPlaceholderItems />
|
||||
</slot>
|
||||
</Carousel>
|
||||
</div>
|
||||
</div>
|
||||
@@ -48,7 +48,7 @@ export function createModalProps(onClose: () => void, onBack?: () => void) {
|
||||
|
||||
return {
|
||||
close,
|
||||
back,
|
||||
back: onBack ? back : undefined,
|
||||
id
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
import { getTmdbMovie } from '$lib/apis/tmdb/tmdbApi';
|
||||
import type { PageServerLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
movie: await getTmdbMovie(Number(params.id))
|
||||
};
|
||||
}) satisfies PageServerLoad;
|
||||
@@ -1,28 +1,13 @@
|
||||
<script lang="ts">
|
||||
import ResourceDetails from '$lib/components/ResourceDetails/ResourceDetails.svelte';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import type { PageData } from './$types';
|
||||
import MoviePage from './MoviePage.svelte';
|
||||
|
||||
export let data: PageData;
|
||||
|
||||
let tmdbId: number;
|
||||
$: tmdbId = Number(data.tmdbId);
|
||||
</script>
|
||||
|
||||
{#await $library then libraryData}
|
||||
{#if data.movie}
|
||||
{@const movie = data.movie}
|
||||
<ResourceDetails
|
||||
tmdbId={movie?.id || 0}
|
||||
type="movie"
|
||||
title={movie?.title || ''}
|
||||
releaseDate={new Date(movie?.release_date || Date.now())}
|
||||
tagline={movie?.tagline || ''}
|
||||
overview={movie?.overview || ''}
|
||||
genres={movie?.genres?.map((g) => g.name || '') || []}
|
||||
runtime={movie?.runtime || 0}
|
||||
tmdbRating={movie?.vote_average || 0}
|
||||
starring={movie?.credits?.cast?.slice(0, 5)}
|
||||
videos={movie.videos?.results || []}
|
||||
backdropPath={movie?.backdrop_path || ''}
|
||||
showDetails={true}
|
||||
jellyfinId={libraryData.items[movie.id]?.jellyfinId}
|
||||
/>
|
||||
{/if}
|
||||
{/await}
|
||||
{#key tmdbId}
|
||||
<MoviePage {tmdbId} />
|
||||
{/key}
|
||||
|
||||
7
src/routes/movie/[id]/+page.ts
Normal file
7
src/routes/movie/[id]/+page.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load = (async ({ params }) => {
|
||||
return {
|
||||
tmdbId: params.id
|
||||
};
|
||||
}) satisfies PageLoad;
|
||||
@@ -1,39 +0,0 @@
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getRadarrMovieByTmdbId, getRadarrDownloadsById } from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
export const _parseMovieId = (params: any) => {
|
||||
const { id: tmdbId } = params;
|
||||
|
||||
if (!tmdbId) throw error(400, 'NO_TMDB_ID');
|
||||
|
||||
return tmdbId;
|
||||
};
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const tmdbId = _parseMovieId(params);
|
||||
|
||||
const jellyfinMoviePromise = getJellyfinItemByTmdbId(tmdbId);
|
||||
const radarrMoviePromise = getRadarrMovieByTmdbId(tmdbId);
|
||||
const radarrMovieQueuedPromise = radarrMoviePromise.then((movie) =>
|
||||
movie?.id ? getRadarrDownloadsById(movie.id) : undefined
|
||||
);
|
||||
|
||||
const [jellyfinItem, radarrMovie, radarrDownloads] = await Promise.all([
|
||||
jellyfinMoviePromise,
|
||||
radarrMoviePromise,
|
||||
radarrMovieQueuedPromise
|
||||
]);
|
||||
|
||||
return json({
|
||||
canStream: !!jellyfinItem,
|
||||
hasLocalFiles: radarrMovie?.hasFile || !!jellyfinItem,
|
||||
isAdded: !!radarrMovie,
|
||||
isDownloading: !!radarrDownloads?.length,
|
||||
|
||||
jellyfinItem,
|
||||
radarrMovie,
|
||||
radarrDownloads
|
||||
});
|
||||
}) satisfies RequestHandler;
|
||||
306
src/routes/movie/[id]/MoviePage.svelte
Normal file
306
src/routes/movie/[id]/MoviePage.svelte
Normal file
@@ -0,0 +1,306 @@
|
||||
<script lang="ts">
|
||||
import { addMovieToRadarr } from '$lib/apis/radarr/radarrApi';
|
||||
import {
|
||||
getTmdbMovie,
|
||||
getTmdbMovieRecommendations,
|
||||
getTmdbMovieSimilar
|
||||
} from '$lib/apis/tmdb/tmdbApi';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import Card from '$lib/components/Card/Card.svelte';
|
||||
import { fetchCardTmdbProps } from '$lib/components/Card/card';
|
||||
import CarouselPlaceholderItems from '$lib/components/Carousel/CarouselPlaceholderItems.svelte';
|
||||
import DetailsPage from '$lib/components/DetailsPage/DetailsPage.svelte';
|
||||
import { createModalProps } from '$lib/components/Modal/Modal';
|
||||
import PeopleCard from '$lib/components/PeopleCard/PeopleCard.svelte';
|
||||
import RequestModal from '$lib/components/RequestModal/RequestModal.svelte';
|
||||
import { playerState } from '$lib/components/VideoPlayer/VideoPlayer';
|
||||
import { createLibraryItemStore, library } from '$lib/stores/library.store';
|
||||
import { formatMinutesToTime, formatSize } from '$lib/utils';
|
||||
import { Archive, ChevronRight, Plus } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
export let tmdbId: number;
|
||||
|
||||
const itemStore = createLibraryItemStore(tmdbId);
|
||||
|
||||
let requestModalVisible = false;
|
||||
const requestModalProps = createModalProps(() => (requestModalVisible = false));
|
||||
|
||||
const tmdbMoviePromise = getTmdbMovie(tmdbId);
|
||||
const tmdbRecommendationProps = getTmdbMovieRecommendations(tmdbId)
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUri));
|
||||
const tmdbSimilarProps = getTmdbMovieSimilar(tmdbId)
|
||||
.then((r) => Promise.all(r.map(fetchCardTmdbProps)))
|
||||
.then((r) => r.filter((p) => p.backdropUri));
|
||||
const castProps: Promise<ComponentProps<PeopleCard>[]> = tmdbMoviePromise.then((m) =>
|
||||
Promise.all(
|
||||
m?.credits?.cast?.map((m) => ({
|
||||
tmdbId: m.id || 0,
|
||||
name: m.name || '',
|
||||
backdropUri: m.profile_path || '',
|
||||
department: m.known_for_department || ''
|
||||
})) || []
|
||||
)
|
||||
);
|
||||
|
||||
function stream() {
|
||||
if ($itemStore.item?.jellyfinItem?.Id)
|
||||
playerState.streamJellyfinId($itemStore.item?.jellyfinItem?.Id);
|
||||
}
|
||||
|
||||
async function refresh() {
|
||||
await library.refresh();
|
||||
}
|
||||
|
||||
let addToRadarrLoading = false;
|
||||
function addToRadarr() {
|
||||
addToRadarrLoading = true;
|
||||
addMovieToRadarr(tmdbId)
|
||||
.then(refresh)
|
||||
.finally(() => (addToRadarrLoading = false));
|
||||
}
|
||||
</script>
|
||||
|
||||
{#await tmdbMoviePromise then movie}
|
||||
<DetailsPage
|
||||
title={movie?.title || 'Movie'}
|
||||
backdropPath={movie?.backdrop_path || ''}
|
||||
posterPath={movie?.poster_path || ''}
|
||||
tagline={movie?.tagline || movie?.title || ''}
|
||||
overview={movie?.overview || ''}
|
||||
>
|
||||
<svelte:fragment slot="title-info-1"
|
||||
>{new Date(movie?.release_date || Date.now()).getFullYear()}</svelte:fragment
|
||||
>
|
||||
<svelte:fragment slot="title-info-2">{movie?.runtime} min</svelte:fragment>
|
||||
<svelte:fragment slot="title-info-3">{movie?.vote_average?.toFixed(1)} TMDB</svelte:fragment>
|
||||
<svelte:fragment slot="episodes-carousel">
|
||||
{@const progress = $itemStore.item?.continueWatching?.progress}
|
||||
{#if progress}
|
||||
<div
|
||||
class="h-1 bg-zinc-800 rounded-full overflow-hidden group-hover:opacity-0 transition-opacity mx-4 sm:mx-8"
|
||||
>
|
||||
<div style={'width: ' + progress + '%'} class="h-full bg-zinc-400" />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="title-right">
|
||||
{#if $itemStore.loading}
|
||||
<div class="placeholder h-10 w-48 rounded-xl" />
|
||||
{:else if $itemStore.item?.jellyfinItem}
|
||||
<Button type="primary" on:click={stream}>
|
||||
<span>Stream</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
{:else if !$itemStore.item?.radarrMovie}
|
||||
<Button type="primary" disabled={addToRadarrLoading} on:click={addToRadarr}>
|
||||
<span>Add to Radarr</span><Plus size={20} />
|
||||
</Button>
|
||||
{:else}
|
||||
<Button type="primary" on:click={() => (requestModalVisible = true)}>
|
||||
<span class="mr-2">Request Movie</span><Plus size={20} />
|
||||
</Button>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="info-components">
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Directed By</p>
|
||||
<h2 class="font-medium">
|
||||
{movie?.credits.crew?.filter((c) => c.job == 'Director').map((p) => p.name)}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Release Date</p>
|
||||
<h2 class="font-medium">
|
||||
{new Date(movie?.release_date || Date.now()).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{#if movie?.budget}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Budget</p>
|
||||
<h2 class="font-medium">
|
||||
{movie?.budget?.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
{#if movie?.revenue}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Revenue</p>
|
||||
<h2 class="font-medium">
|
||||
{movie?.revenue?.toLocaleString('en-US', {
|
||||
style: 'currency',
|
||||
currency: 'USD'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Status</p>
|
||||
<h2 class="font-medium">
|
||||
{movie?.status}
|
||||
</h2>
|
||||
</div>
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Runtime</p>
|
||||
<h2 class="font-medium">
|
||||
{movie?.runtime} Minutes
|
||||
</h2>
|
||||
</div>
|
||||
<!-- {#if series?.first_air_date}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">First Air Date</p>
|
||||
<h2 class="font-medium">
|
||||
{new Date(series?.first_air_date).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
{#if series?.next_episode_to_air}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Next Air Date</p>
|
||||
<h2 class="font-medium">
|
||||
{new Date(series.next_episode_to_air?.air_date).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{:else if series?.last_air_date}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Last Air Date</p>
|
||||
<h2 class="font-medium">
|
||||
{new Date(series.last_air_date).toLocaleDateString('en', {
|
||||
year: 'numeric',
|
||||
month: 'short',
|
||||
day: 'numeric'
|
||||
})}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Networks</p>
|
||||
<h2 class="font-medium">{series?.networks?.map((n) => n.name).join(', ')}</h2>
|
||||
</div>
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Episode Run Time</p>
|
||||
<h2 class="font-medium">{series?.episode_run_time} Minutes</h2>
|
||||
</div>
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Spoken Languages</p>
|
||||
<h2 class="font-medium">
|
||||
{series?.spoken_languages?.map((l) => capitalize(l.name || '')).join(', ')}
|
||||
</h2>
|
||||
</div> -->
|
||||
</svelte:fragment>
|
||||
|
||||
<svelte:fragment slot="servarr-components">
|
||||
{#if !$itemStore.loading && $itemStore.item}
|
||||
{@const item = $itemStore.item}
|
||||
{#if item.radarrMovie?.movieFile?.quality}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Video</p>
|
||||
<h2 class="font-medium">
|
||||
{item.radarrMovie?.movieFile?.quality.quality?.name}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
{#if item.radarrMovie?.movieFile?.size}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Size On Disk</p>
|
||||
<h2 class="font-medium">
|
||||
{formatSize(item.radarrMovie?.movieFile?.size || 0)}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
{#if $itemStore.item?.download}
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Download Completed In</p>
|
||||
<h2 class="font-medium">
|
||||
{formatMinutesToTime(
|
||||
(new Date($itemStore.item?.download.completionTime).getTime() - Date.now()) /
|
||||
1000 /
|
||||
60
|
||||
)}
|
||||
</h2>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<div class="flex gap-4 flex-wrap col-span-4 sm:col-span-6 mt-4">
|
||||
<Button on:click={() => (requestModalVisible = true)}>
|
||||
<span class="mr-2">Request Movie</span><Plus size={20} />
|
||||
</Button>
|
||||
<Button>
|
||||
<span class="mr-2">Manage</span><Archive size={20} />
|
||||
</Button>
|
||||
</div>
|
||||
<!-- <div
|
||||
class="flex-1 flex justify-between py-2 gap-4 items-start sm:items-center flex-col sm:flex-row"
|
||||
>
|
||||
<div>
|
||||
<h1 class="font-medium text-lg">No sources found</h1>
|
||||
<p class="text-sm text-zinc-400">Check your source settings</p>
|
||||
</div>
|
||||
<Button type="secondary"><span>Add to Sonarr</span><Plus size={20} /></Button>
|
||||
</div> -->
|
||||
{:else if $itemStore.loading}
|
||||
<div class="flex gap-4 flex-wrap col-span-4 sm:col-span-6 mt-4">
|
||||
<div class="placeholder h-10 w-40 rounded-xl" />
|
||||
<div class="placeholder h-10 w-40 rounded-xl" />
|
||||
</div>
|
||||
{/if}
|
||||
</svelte:fragment>
|
||||
|
||||
<div slot="cast-crew-carousel-title" class="font-medium text-lg">Cast & Crew</div>
|
||||
<svelte:fragment slot="cast-crew-carousel">
|
||||
{#await castProps}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop}
|
||||
<PeopleCard {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
|
||||
<div slot="recommendations-carousel-title" class="font-medium text-lg">Recommendations</div>
|
||||
<svelte:fragment slot="recommendations-carousel">
|
||||
{#await tmdbRecommendationProps}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
|
||||
<div slot="similar-carousel-title" class="font-medium text-lg">Similar Titles</div>
|
||||
<svelte:fragment slot="similar-carousel">
|
||||
{#await tmdbSimilarProps}
|
||||
<CarouselPlaceholderItems />
|
||||
{:then props}
|
||||
{#each props as prop}
|
||||
<Card {...prop} />
|
||||
{/each}
|
||||
{/await}
|
||||
</svelte:fragment>
|
||||
</DetailsPage>
|
||||
{/await}
|
||||
|
||||
{#if requestModalVisible}
|
||||
{@const radarrMovie = $itemStore.item?.radarrMovie}
|
||||
{#if radarrMovie && radarrMovie.id && radarrMovie?.movieFile}
|
||||
<RequestModal modalProps={requestModalProps} radarrId={radarrMovie.id} />
|
||||
{/if}
|
||||
{/if}
|
||||
@@ -1,13 +0,0 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { _parseMovieId } from '../+server';
|
||||
import { addMovieToRadarr, deleteRadarrMovie } from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
// Delete download
|
||||
export const DELETE = (async ({ params }) => {
|
||||
const radarrMovieId = _parseMovieId(params);
|
||||
|
||||
const success = await deleteRadarrMovie(radarrMovieId);
|
||||
|
||||
return json({ success });
|
||||
}) satisfies RequestHandler;
|
||||
@@ -1,21 +0,0 @@
|
||||
import { _parseMovieId } from '../+server';
|
||||
import { addMovieToRadarr, getRadarrMovieByTmdbId } from '$lib/apis/radarr/radarrApi';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
|
||||
// Add to radarr
|
||||
export const POST = (async ({ params }) => {
|
||||
const tmdbId = _parseMovieId(params);
|
||||
|
||||
const response = await addMovieToRadarr(Number(tmdbId));
|
||||
|
||||
return json(response);
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const tmdbId = _parseMovieId(params);
|
||||
|
||||
const response = await getRadarrMovieByTmdbId(tmdbId);
|
||||
|
||||
return json(response);
|
||||
}) satisfies RequestHandler;
|
||||
@@ -1,50 +0,0 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { _parseMovieId } from '../+server';
|
||||
import {
|
||||
cancelDownloadRadarrMovie,
|
||||
addMovieToRadarr,
|
||||
fetchRadarrReleases,
|
||||
downloadRadarrMovie
|
||||
} from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const radarrId = _parseMovieId(params);
|
||||
|
||||
const releases: any[] = (await fetchRadarrReleases(radarrId)) || [];
|
||||
|
||||
let filtered = releases.slice();
|
||||
|
||||
filtered.sort((a, b) => b.seeders - a.seeders);
|
||||
filtered = filtered.filter((release) => release.quality.quality.resolution > 720).slice(0, 5);
|
||||
|
||||
const releasesSkipped = releases.length - filtered.length;
|
||||
|
||||
releases.sort((a, b) => b.size - a.size);
|
||||
filtered.sort((a, b) => b.size - a.size);
|
||||
|
||||
return json({
|
||||
filtered,
|
||||
releasesSkipped,
|
||||
allReleases: releases
|
||||
});
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
// Download movie
|
||||
export const POST = (async ({ params, request }) => {
|
||||
const body = await request.json();
|
||||
|
||||
if (!body.guid) throw new Error('NO_GUID');
|
||||
|
||||
const response = await downloadRadarrMovie(body.guid);
|
||||
|
||||
return json(response);
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
export const DELETE = (async ({ params }) => {
|
||||
const downloadId = _parseMovieId(params);
|
||||
|
||||
const success = await cancelDownloadRadarrMovie(downloadId);
|
||||
|
||||
return json({ success });
|
||||
}) satisfies RequestHandler;
|
||||
@@ -23,7 +23,7 @@
|
||||
import { capitalize, formatMinutesToTime, formatSize } from '$lib/utils';
|
||||
import classNames from 'classnames';
|
||||
import { Archive, ChevronLeft, ChevronRight, DotFilled, Plus } from 'radix-icons-svelte';
|
||||
import { tick, type ComponentProps, onMount } from 'svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
|
||||
export let tmdbId: number;
|
||||
@@ -35,7 +35,10 @@
|
||||
let visibleEpisodeIndex: number | undefined = undefined;
|
||||
|
||||
let requestModalVisible = false;
|
||||
const requestModalProps = createModalProps(() => (requestModalVisible = false));
|
||||
const requestModalProps = createModalProps(
|
||||
() => (requestModalVisible = false),
|
||||
() => {}
|
||||
);
|
||||
|
||||
let episodeProps: ComponentProps<EpisodeCard>[][] = [];
|
||||
let episodeComponents: HTMLDivElement[] = [];
|
||||
@@ -104,12 +107,12 @@
|
||||
await library.refresh();
|
||||
}
|
||||
|
||||
let addToRadarrLoading = false;
|
||||
let addToSonarrLoading = false;
|
||||
function addToSonarr() {
|
||||
addToRadarrLoading = true;
|
||||
addToSonarrLoading = true;
|
||||
addSeriesToSonarr(tmdbId)
|
||||
.then(refresh)
|
||||
.finally(() => (addToRadarrLoading = false));
|
||||
.finally(() => (addToSonarrLoading = false));
|
||||
}
|
||||
|
||||
let didFocusNextEpisode = false;
|
||||
@@ -174,7 +177,7 @@
|
||||
<span>Next Episode</span><ChevronRight size={20} />
|
||||
</Button>
|
||||
{:else if !$itemStore.item?.sonarrSeries}
|
||||
<Button type="primary" disabled={addToRadarrLoading} on:click={addToSonarr}>
|
||||
<Button type="primary" disabled={addToSonarrLoading} on:click={addToSonarr}>
|
||||
<span>Add to Sonarr</span><Plus size={20} />
|
||||
</Button>
|
||||
{:else}
|
||||
@@ -344,7 +347,7 @@
|
||||
<div class="col-span-2 lg:col-span-1">
|
||||
<p class="text-zinc-400 text-sm">Spoken Languages</p>
|
||||
<h2 class="font-medium">
|
||||
{series?.spoken_languages?.map((l) => capitalize(l.name || '')).join(', ')}
|
||||
{series?.spoken_languages?.map((l) => capitalize(l.english_name || '')).join(', ')}
|
||||
</h2>
|
||||
</div>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user