Library page improvements
This commit is contained in:
@@ -21,3 +21,4 @@ Further ideas
|
||||
|
||||
- [ ] Similar movies & shows, actor pages and recommendations
|
||||
- [ ] Watchlist management
|
||||
- [ ] Download a movie file
|
||||
|
||||
@@ -47,7 +47,7 @@ export const getRadarrMovieByTmdbId = (tmdbId: string): Promise<RadarrMovie | un
|
||||
}
|
||||
}).then((r) => r.data?.find((m) => (m.tmdbId as any) == tmdbId));
|
||||
|
||||
export const addRadarrMovie = async (tmdbId: string) => {
|
||||
export const addRadarrMovie = async (tmdbId: number) => {
|
||||
const tmdbMovie = await getTmdbMovie(tmdbId);
|
||||
const radarrMovie = await lookupRadarrMovieByTmdbId(tmdbId);
|
||||
console.log('fetched movies', tmdbMovie, radarrMovie);
|
||||
@@ -62,9 +62,9 @@ export const addRadarrMovie = async (tmdbId: string) => {
|
||||
profileId: qualityProfile,
|
||||
rootFolderPath: '/movies',
|
||||
minimumAvailability: 'announced',
|
||||
title: tmdbMovie.title,
|
||||
tmdbId: tmdbMovie.id,
|
||||
year: Number((await tmdbMovie).release_date.slice(0, 4)),
|
||||
title: tmdbMovie.title || tmdbMovie.original_title || '',
|
||||
tmdbId: tmdbMovie.id || 0,
|
||||
year: Number(tmdbMovie.release_date?.slice(0, 4)),
|
||||
monitored: false,
|
||||
tags: [],
|
||||
searchNow: false
|
||||
@@ -124,14 +124,14 @@ export const getRadarrDownloads = (): Promise<RadarrDownload[]> =>
|
||||
}
|
||||
}).then((r) => (r.data?.records?.filter((record) => record.movie) as RadarrDownload[]) || []);
|
||||
|
||||
export const getRadarrDownloadById = (radarrId: number) =>
|
||||
getRadarrDownloads().then((downloads) => downloads.find((d) => d.movie.id === radarrId));
|
||||
export const getRadarrDownloadsById = (radarrId: number) =>
|
||||
getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId));
|
||||
|
||||
const lookupRadarrMovieByTmdbId = (tmdbId: string) =>
|
||||
const lookupRadarrMovieByTmdbId = (tmdbId: number) =>
|
||||
RadarrApi.get('/api/v3/movie/lookup/tmdb', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(tmdbId)
|
||||
tmdbId
|
||||
}
|
||||
}
|
||||
}).then((r) => r.data as any as RadarrMovie);
|
||||
|
||||
@@ -5,16 +5,18 @@
|
||||
import { Clock, Star } from 'radix-icons-svelte';
|
||||
|
||||
export let tmdbId: string;
|
||||
export let type: 'movie' | 'series' = 'movie';
|
||||
export let title: string;
|
||||
export let genres: string[];
|
||||
export let runtimeMinutes: number;
|
||||
export let runtimeMinutes = 0;
|
||||
export let seasons = 0;
|
||||
export let completionTime = '';
|
||||
export let backdropUrl: string;
|
||||
export let rating: number;
|
||||
|
||||
export let available = true;
|
||||
export let progress = 0;
|
||||
export let type: 'dynamic' | 'normal' | 'large' = 'normal';
|
||||
export let size: 'dynamic' | 'normal' | 'large' = 'normal';
|
||||
export let randomProgress = false;
|
||||
if (randomProgress) {
|
||||
progress = Math.random() > 0.3 ? Math.random() * 100 : 0;
|
||||
@@ -23,15 +25,15 @@
|
||||
|
||||
<div
|
||||
class={classNames('rounded overflow-hidden relative shadow-2xl shrink-0 aspect-video', {
|
||||
'h-40': type === 'normal',
|
||||
'h-60': type === 'large',
|
||||
'w-full': type === 'dynamic'
|
||||
'h-40': size === 'normal',
|
||||
'h-60': size === 'large',
|
||||
'w-full': size === 'dynamic'
|
||||
})}
|
||||
>
|
||||
<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('/movie/' + tmdbId, '_self')}
|
||||
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;' : ''}
|
||||
>
|
||||
@@ -59,6 +61,11 @@
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
{#if seasons}
|
||||
<div class="text-sm text-zinc-200">
|
||||
{seasons} seasons
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if rating}
|
||||
<div class="flex gap-1.5 items-center">
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
<CarouselPlaceholderItems type="large" />
|
||||
{:then { popularMovies: movies }}
|
||||
{#each movies ? [...movies].reverse() : [] as movie (movie.tmdbId)}
|
||||
<Card type="large" {...movie} />
|
||||
<Card size="large" {...movie} />
|
||||
{/each}
|
||||
{/await}
|
||||
</Carousel>
|
||||
|
||||
@@ -4,8 +4,13 @@
|
||||
import IconButton from '$lib/components/IconButton.svelte';
|
||||
import RadarrStats from '$lib/components/SourceStats/RadarrStats.svelte';
|
||||
import SonarrStats from '$lib/components/SourceStats/SonarrStats.svelte';
|
||||
import { library } from '$lib/stores/library.store';
|
||||
import {
|
||||
library,
|
||||
type PlayableRadarrMovie,
|
||||
type PlayableSonarrSeries
|
||||
} from '$lib/stores/library.store';
|
||||
import { ChevronDown, MagnifyingGlass, TextAlignBottom, Trash } from 'radix-icons-svelte';
|
||||
import type { ComponentProps } from 'svelte';
|
||||
|
||||
const watched = [];
|
||||
|
||||
@@ -14,11 +19,90 @@
|
||||
const headerStyle = 'uppercase tracking-widest font-bold';
|
||||
const headerContaienr = 'flex items-center justify-between mt-2';
|
||||
|
||||
function sortByAdded(arr: any[]) {
|
||||
let itemsVisible: 'all' | 'movies' | 'shows' = 'all';
|
||||
let sortBy: 'added' | 'rating' | 'year' | 'size' | 'name' = 'added';
|
||||
|
||||
let downloadingProps: ComponentProps<Card>[] = [];
|
||||
let availableProps: ComponentProps<Card>[] = [];
|
||||
let unavailableProps: ComponentProps<Card>[] = [];
|
||||
|
||||
function itemIsSeries(
|
||||
item: PlayableRadarrMovie | PlayableSonarrSeries
|
||||
): item is PlayableSonarrSeries {
|
||||
return (item as PlayableSonarrSeries).seasons !== undefined;
|
||||
}
|
||||
|
||||
function itemIsMovie(
|
||||
item: PlayableRadarrMovie | PlayableSonarrSeries
|
||||
): item is PlayableRadarrMovie {
|
||||
return (item as PlayableRadarrMovie).isAvailable !== undefined;
|
||||
}
|
||||
|
||||
library.subscribe(async (libraryPromise) => {
|
||||
const libraryData = await libraryPromise;
|
||||
|
||||
const items = filterItems(sortItems([...libraryData.movies, ...libraryData.series]));
|
||||
|
||||
for (let item of items) {
|
||||
let props: ComponentProps<Card>;
|
||||
if (itemIsSeries(item)) {
|
||||
props = {
|
||||
size: 'dynamic',
|
||||
type: 'series',
|
||||
tmdbId: String(item.tmdbId),
|
||||
title: item.title || '',
|
||||
genres: item.genres || [],
|
||||
backdropUrl: item.cardBackdropUrl,
|
||||
rating: item.ratings?.tmdb?.value || item.ratings?.imdb?.value || 7.5,
|
||||
seasons: item.seasons?.length || 0
|
||||
};
|
||||
} else if (itemIsMovie(item)) {
|
||||
props = {
|
||||
size: 'dynamic',
|
||||
type: 'movie',
|
||||
tmdbId: String(item.tmdbId),
|
||||
title: item.title || '',
|
||||
genres: item.genres || [],
|
||||
backdropUrl: item.cardBackdropUrl,
|
||||
rating: item.ratings?.tmdb?.value || item.ratings?.imdb?.value || 7.5,
|
||||
runtimeMinutes: item.runtime || 0
|
||||
};
|
||||
} else {
|
||||
console.log('RETURNING');
|
||||
return;
|
||||
}
|
||||
|
||||
if (item.download) {
|
||||
downloadingProps.push({
|
||||
...props,
|
||||
progress: item.download.progress,
|
||||
completionTime: item.download.completionTime
|
||||
});
|
||||
} else if (
|
||||
((item as PlayableRadarrMovie)?.isAvailable && (item as PlayableRadarrMovie)?.movieFile) ||
|
||||
(item as PlayableSonarrSeries)?.seasons?.find(
|
||||
(season) => !!season?.statistics?.episodeFileCount
|
||||
)
|
||||
) {
|
||||
console.log(item);
|
||||
availableProps.push(props);
|
||||
} else {
|
||||
unavailableProps.push({ ...props, available: false });
|
||||
}
|
||||
}
|
||||
|
||||
downloadingProps = downloadingProps;
|
||||
availableProps = availableProps;
|
||||
unavailableProps = unavailableProps;
|
||||
});
|
||||
|
||||
function sortItems(arr: any[]) {
|
||||
return arr.sort((a, b) => ((a.added || '') > (b.added || '') ? -1 : 1));
|
||||
}
|
||||
|
||||
console.log($library);
|
||||
function filterItems(arr: any[]) {
|
||||
return arr;
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="pt-24 pb-8 px-8 bg-black">
|
||||
@@ -81,38 +165,102 @@
|
||||
{/each}
|
||||
</div>
|
||||
{:then libraryData}
|
||||
{@const downloading = sortByAdded([
|
||||
<!-- {@const downloading = sortItems([
|
||||
...libraryData.movies.filter((m) => !!m.download),
|
||||
...libraryData.series.filter((s) => !!s.download)
|
||||
])}
|
||||
{@const available = sortByAdded([
|
||||
{@const available = sortItems([
|
||||
...libraryData.movies.filter((m) => !m.download && m.movieFile && m.isAvailable),
|
||||
...libraryData.series.filter(
|
||||
(s) => !s.download && s.seasons?.find((season) => !!season?.statistics?.episodeFileCount)
|
||||
)
|
||||
])}
|
||||
{@const unavailable = sortByAdded([
|
||||
{@const unavailable = sortItems([
|
||||
...libraryData.movies.filter((m) => !m.download && (!m.movieFile || !m.isAvailable)),
|
||||
...libraryData.series.filter(
|
||||
(s) => !s.download && !s.seasons?.find((season) => !!season?.statistics?.episodeFileCount)
|
||||
)
|
||||
])}
|
||||
])} -->
|
||||
|
||||
{#if downloading.length > 0}
|
||||
{#if downloadingProps.length > 0}
|
||||
<div class={headerContaienr}>
|
||||
<h1 class={headerStyle}>
|
||||
Downloading <span class="text-zinc-500">{downloading.length}</span>
|
||||
Downloading <span class="text-zinc-500">{downloadingProps.length}</span>
|
||||
</h1>
|
||||
<IconButton>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class={posterGridStyle}>
|
||||
{#each downloading as movie (movie)}
|
||||
{#each downloadingProps as props}
|
||||
<Card {...props} />
|
||||
{/each}
|
||||
<!-- {#each downloading as item (item)}
|
||||
{@const series = 'seasons' in item}
|
||||
<Card
|
||||
type="dynamic"
|
||||
progress={movie.download?.progress || 0}
|
||||
completionTime={movie.download?.completionTime}
|
||||
size="dynamic"
|
||||
type={series ? 'series' : 'movie'}
|
||||
progress={item.download?.progress || 0}
|
||||
completionTime={item.download?.completionTime}
|
||||
available={false}
|
||||
tmdbId={String(item.tmdbId)}
|
||||
title={item.title || ''}
|
||||
genres={item.genres || []}
|
||||
backdropUrl={item.cardBackdropUrl}
|
||||
rating={item.ratings?.tmdb?.value || item.ratings?.imdb?.value || 0}
|
||||
runtimeMinutes={series ? 0 : item.runtime || 0}
|
||||
seasons={series ? item.seasons?.length : 0}
|
||||
/>
|
||||
{/each} -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if availableProps.length > 0}
|
||||
<div class={headerContaienr}>
|
||||
<h1 class={headerStyle}>
|
||||
Available <span class="text-zinc-500">{availableProps.length}</span>
|
||||
</h1>
|
||||
<IconButton>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class={posterGridStyle}>
|
||||
{#each availableProps as props}
|
||||
<Card {...props} />
|
||||
{/each}
|
||||
<!-- {#each available as item (item)}
|
||||
{@const series = 'seasons' in item}
|
||||
<Card
|
||||
size="dynamic"
|
||||
type={series ? 'series' : 'movie'}
|
||||
tmdbId={String(item.tmdbId)}
|
||||
title={item.title || ''}
|
||||
genres={item.genres || []}
|
||||
backdropUrl={item.cardBackdropUrl}
|
||||
rating={item.ratings?.tmdb?.value || item.ratings?.imdb?.value || 7.5}
|
||||
runtimeMinutes={'seasons' in item ? 0 : item.runtime || 0}
|
||||
seasons={'seasons' in item ? item.seasons?.length : 0}
|
||||
/>
|
||||
{/each} -->
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if unavailableProps.length > 0}
|
||||
<div class={headerContaienr}>
|
||||
<h1 class={headerStyle}>
|
||||
Unavailable <span class="text-zinc-500">{unavailableProps.length}</span>
|
||||
</h1>
|
||||
<IconButton>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class={posterGridStyle}>
|
||||
{#each unavailableProps as props}
|
||||
<Card {...props} />
|
||||
{/each}
|
||||
<!-- {#each unavailable as movie (movie.tmdbId)}
|
||||
<Card
|
||||
size="dynamic"
|
||||
available={false}
|
||||
tmdbId={String(movie.tmdbId)}
|
||||
title={movie.title || ''}
|
||||
@@ -121,56 +269,7 @@
|
||||
backdropUrl={movie.cardBackdropUrl}
|
||||
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if available.length > 0}
|
||||
<div class={headerContaienr}>
|
||||
<h1 class={headerStyle}>
|
||||
Available <span class="text-zinc-500">{available.length}</span>
|
||||
</h1>
|
||||
<IconButton>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class={posterGridStyle}>
|
||||
{#each available as movie (movie.tmdbId)}
|
||||
<Card
|
||||
type="dynamic"
|
||||
tmdbId={String(movie.tmdbId)}
|
||||
title={movie.title || ''}
|
||||
genres={movie.genres || []}
|
||||
runtimeMinutes={movie.runtime || 0}
|
||||
backdropUrl={movie.cardBackdropUrl}
|
||||
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if unavailable.length > 0}
|
||||
<div class={headerContaienr}>
|
||||
<h1 class={headerStyle}>
|
||||
Unavailable <span class="text-zinc-500">{unavailable.length}</span>
|
||||
</h1>
|
||||
<IconButton>
|
||||
<ChevronDown size={24} />
|
||||
</IconButton>
|
||||
</div>
|
||||
<div class={posterGridStyle}>
|
||||
{#each unavailable as movie (movie.tmdbId)}
|
||||
<Card
|
||||
type="dynamic"
|
||||
available={false}
|
||||
tmdbId={String(movie.tmdbId)}
|
||||
title={movie.title || ''}
|
||||
genres={movie.genres || []}
|
||||
runtimeMinutes={movie.runtime || 0}
|
||||
backdropUrl={movie.cardBackdropUrl}
|
||||
rating={movie.ratings?.tmdb?.value || movie.ratings?.imdb?.value || 0}
|
||||
/>
|
||||
{/each}
|
||||
{/each} -->
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { error, json } from '@sveltejs/kit';
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { getJellyfinItemByTmdbId } from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import { getRadarrMovieByTmdbId, getRadarrDownloadById } from '$lib/apis/radarr/radarrApi';
|
||||
import { getRadarrMovieByTmdbId, getRadarrDownloadsById } from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
export const parseMovieId = (params: any) => {
|
||||
export const _parseMovieId = (params: any) => {
|
||||
const { id: tmdbId } = params;
|
||||
|
||||
if (!tmdbId) throw error(400, 'NO_TMDB_ID');
|
||||
@@ -12,12 +12,12 @@ export const parseMovieId = (params: any) => {
|
||||
};
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const tmdbId = parseMovieId(params);
|
||||
const tmdbId = _parseMovieId(params);
|
||||
|
||||
const jellyfinMoviePromise = getJellyfinItemByTmdbId(tmdbId);
|
||||
const radarrMoviePromise = getRadarrMovieByTmdbId(tmdbId);
|
||||
const radarrMovieQueuedPromise = radarrMoviePromise.then((movie) =>
|
||||
movie?.id ? getRadarrDownloadById(movie.id) : undefined
|
||||
movie?.id ? getRadarrDownloadsById(movie.id) : undefined
|
||||
);
|
||||
|
||||
const [jellyfinItem, radarrMovie, radarrDownloads] = await Promise.all([
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { parseMovieId } from '../+server';
|
||||
import { _parseMovieId } from '../+server';
|
||||
import { addRadarrMovie, deleteRadarrMovie } from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
// Delete download
|
||||
export const DELETE = (async ({ params }) => {
|
||||
const radarrMovieId = parseMovieId(params);
|
||||
const radarrMovieId = _parseMovieId(params);
|
||||
|
||||
const success = await deleteRadarrMovie(radarrMovieId);
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
import { parseMovieId } from '../+server';
|
||||
import { _parseMovieId } from '../+server';
|
||||
import { addRadarrMovie, 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 tmdbId = _parseMovieId(params);
|
||||
|
||||
const response = await addRadarrMovie(tmdbId);
|
||||
const response = await addRadarrMovie(Number(tmdbId));
|
||||
|
||||
return json(response);
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const tmdbId = parseMovieId(params);
|
||||
const tmdbId = _parseMovieId(params);
|
||||
|
||||
const response = await getRadarrMovieByTmdbId(tmdbId);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { RequestHandler } from '@sveltejs/kit';
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { parseMovieId } from '../+server';
|
||||
import { _parseMovieId } from '../+server';
|
||||
import {
|
||||
cancelDownloadRadarrMovie,
|
||||
addRadarrMovie,
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
} from '$lib/apis/radarr/radarrApi';
|
||||
|
||||
export const GET = (async ({ params }) => {
|
||||
const radarrId = parseMovieId(params);
|
||||
const radarrId = _parseMovieId(params);
|
||||
|
||||
const releases: any[] = (await fetchRadarrReleases(radarrId)) || [];
|
||||
|
||||
@@ -42,7 +42,7 @@ export const POST = (async ({ params, request }) => {
|
||||
}) satisfies RequestHandler;
|
||||
|
||||
export const DELETE = (async ({ params }) => {
|
||||
const downloadId = parseMovieId(params);
|
||||
const downloadId = _parseMovieId(params);
|
||||
|
||||
const success = await cancelDownloadRadarrMovie(downloadId);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user