Library page improvements

This commit is contained in:
Aleksi Lassila
2023-07-11 13:52:49 +03:00
parent 7a738c0459
commit 75b250258c
9 changed files with 198 additions and 91 deletions

View File

@@ -21,3 +21,4 @@ Further ideas
- [ ] Similar movies & shows, actor pages and recommendations
- [ ] Watchlist management
- [ ] Download a movie file

View 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);

View File

@@ -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">

View File

@@ -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>

View File

@@ -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}

View File

@@ -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([

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);