feat: Managing movie files, requesting movies
This commit is contained in:
@@ -8,17 +8,26 @@ import { log } from '../../utils';
|
|||||||
import { appState } from '../../stores/app-state.store';
|
import { appState } from '../../stores/app-state.store';
|
||||||
import type { Api } from '../api.interface';
|
import type { Api } from '../api.interface';
|
||||||
|
|
||||||
|
export const movieAvailabilities = [
|
||||||
|
// 'tba',
|
||||||
|
'announced',
|
||||||
|
'inCinemas',
|
||||||
|
'released'
|
||||||
|
// 'deleted'
|
||||||
|
] as const;
|
||||||
|
|
||||||
export type RadarrMovie = components['schemas']['MovieResource'];
|
export type RadarrMovie = components['schemas']['MovieResource'];
|
||||||
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
||||||
export type RadarrRelease = components['schemas']['ReleaseResource'];
|
export type RadarrRelease = components['schemas']['ReleaseResource'];
|
||||||
export type MovieDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
|
export type MovieDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
|
||||||
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
||||||
export type MovieHistoryResource = components['schemas']['HistoryResource'];
|
export type MovieHistoryResource = components['schemas']['HistoryResource'];
|
||||||
|
export type MovieAvailability = components['schemas']['MovieStatusType'];
|
||||||
|
|
||||||
export interface RadarrMovieOptions {
|
export interface RadarrMovieOptions {
|
||||||
title: string;
|
title: string;
|
||||||
qualityProfileId: number;
|
qualityProfileId: number;
|
||||||
minimumAvailability: 'announced' | 'inCinemas' | 'released';
|
minimumAvailability: MovieAvailability;
|
||||||
tags: number[];
|
tags: number[];
|
||||||
profileId: number;
|
profileId: number;
|
||||||
year: number;
|
year: number;
|
||||||
@@ -68,19 +77,26 @@ export class RadarrApi implements Api<paths> {
|
|||||||
})
|
})
|
||||||
.then((r) => r.data || []) || Promise.resolve([]);
|
.then((r) => r.data || []) || Promise.resolve([]);
|
||||||
|
|
||||||
addMovieToRadarr = async (tmdbId: number) => {
|
addMovieToRadarr = async (
|
||||||
|
tmdbId: number,
|
||||||
|
_options: {
|
||||||
|
qualityProfileId?: number;
|
||||||
|
rootFolderPath?: string;
|
||||||
|
minimumAvailability?: MovieAvailability;
|
||||||
|
} = {}
|
||||||
|
) => {
|
||||||
const tmdbMovie = await getTmdbMovie(tmdbId);
|
const tmdbMovie = await getTmdbMovie(tmdbId);
|
||||||
const radarrMovie = await this.lookupRadarrMovieByTmdbId(tmdbId);
|
// const radarrMovie = await this.lookupRadarrMovieByTmdbId(tmdbId);
|
||||||
|
//
|
||||||
if (radarrMovie?.id) throw new Error('Movie already exists');
|
// if (radarrMovie?.id) throw new Error('Movie already exists');
|
||||||
|
|
||||||
if (!tmdbMovie) throw new Error('Movie not found');
|
if (!tmdbMovie) throw new Error('Movie not found');
|
||||||
|
|
||||||
const options: RadarrMovieOptions = {
|
const options: RadarrMovieOptions = {
|
||||||
qualityProfileId: get(appState).user?.settings.radarr.qualityProfileId || 0,
|
qualityProfileId: _options.qualityProfileId || this.getSettings()?.qualityProfileId || 0,
|
||||||
profileId: get(appState).user?.settings.radarr?.qualityProfileId || 0,
|
profileId: _options.qualityProfileId || this.getSettings()?.qualityProfileId || 0,
|
||||||
rootFolderPath: get(appState).user?.settings.radarr.rootFolderPath || '',
|
rootFolderPath: _options.rootFolderPath || this.getSettings()?.rootFolderPath || '',
|
||||||
minimumAvailability: 'announced',
|
minimumAvailability: _options.minimumAvailability || 'released',
|
||||||
title: tmdbMovie.title || tmdbMovie.original_title || '',
|
title: tmdbMovie.title || tmdbMovie.original_title || '',
|
||||||
tmdbId: tmdbMovie.id || 0,
|
tmdbId: tmdbMovie.id || 0,
|
||||||
year: Number(tmdbMovie.release_date?.slice(0, 4)),
|
year: Number(tmdbMovie.release_date?.slice(0, 4)),
|
||||||
@@ -117,6 +133,15 @@ export class RadarrApi implements Api<paths> {
|
|||||||
return !!deleteResponse?.response.ok;
|
return !!deleteResponse?.response.ok;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
cancelDownloads = async (downloadIds: number[]) =>
|
||||||
|
this.getClient()
|
||||||
|
?.DELETE('/api/v3/queue/bulk', {
|
||||||
|
body: {
|
||||||
|
ids: downloadIds
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((r) => r.response.ok) || Promise.resolve(false);
|
||||||
|
|
||||||
getReleases = (movieId: number): Promise<RadarrRelease[]> =>
|
getReleases = (movieId: number): Promise<RadarrRelease[]> =>
|
||||||
this.getClient()
|
this.getClient()
|
||||||
?.GET('/api/v3/release', { params: { query: { movieId: movieId } } })
|
?.GET('/api/v3/release', { params: { query: { movieId: movieId } } })
|
||||||
@@ -165,6 +190,15 @@ export class RadarrApi implements Api<paths> {
|
|||||||
})
|
})
|
||||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||||
|
|
||||||
|
deleteFiles = (ids: number[]) =>
|
||||||
|
this.getClient()
|
||||||
|
?.DELETE('/api/v3/moviefile/bulk', {
|
||||||
|
body: {
|
||||||
|
movieFileIds: ids
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||||
|
|
||||||
getRadarrDownloads = (): Promise<MovieDownload[]> =>
|
getRadarrDownloads = (): Promise<MovieDownload[]> =>
|
||||||
this.getClient()
|
this.getClient()
|
||||||
?.GET('/api/v3/queue', {
|
?.GET('/api/v3/queue', {
|
||||||
|
|||||||
279
src/lib/components/MediaManagerModal/MMAddToRadarrDialog.svelte
Normal file
279
src/lib/components/MediaManagerModal/MMAddToRadarrDialog.svelte
Normal file
@@ -0,0 +1,279 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import Dialog from '../Dialog/Dialog.svelte';
|
||||||
|
import { TMDB_BACKDROP_SMALL } from '../../constants';
|
||||||
|
import { type BackEvent, scrollIntoView, type Selectable } from '../../selectable';
|
||||||
|
import { createLocalStorageStore } from '../../stores/localstorage.store';
|
||||||
|
import {
|
||||||
|
movieAvailabilities,
|
||||||
|
type MovieAvailability,
|
||||||
|
radarrApi
|
||||||
|
} from '../../apis/radarr/radarr-api';
|
||||||
|
import { modalStack } from '../Modal/modal.store';
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { fade } from 'svelte/transition';
|
||||||
|
import Container from '../../../Container.svelte';
|
||||||
|
import { capitalize, formatSize } from '../../utils';
|
||||||
|
import { ArrowRight, Check, Plus } from 'radix-icons-svelte';
|
||||||
|
import Button from '../Button.svelte';
|
||||||
|
|
||||||
|
type AddOptionsStore = {
|
||||||
|
rootFolderPath: string | null;
|
||||||
|
qualityProfileId: number | null;
|
||||||
|
minimumAvailability: MovieAvailability | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export let backdropUri: string;
|
||||||
|
export let tmdbId: number;
|
||||||
|
export let title: string;
|
||||||
|
export let onComplete: () => void = () => {};
|
||||||
|
|
||||||
|
export let modalId: symbol;
|
||||||
|
$: backgroundUrl = TMDB_BACKDROP_SMALL + backdropUri;
|
||||||
|
|
||||||
|
let tab: 'add-to-radarr' | 'root-folders' | 'quality-profiles' | 'monitor-settings' =
|
||||||
|
'add-to-radarr';
|
||||||
|
let addToSonarrTab: Selectable;
|
||||||
|
let rootFoldersTab: Selectable;
|
||||||
|
let qualityProfilesTab: Selectable;
|
||||||
|
let monitorSettingsTab: Selectable;
|
||||||
|
$: {
|
||||||
|
if (tab === 'add-to-radarr' && addToSonarrTab) addToSonarrTab.focus();
|
||||||
|
if (tab === 'root-folders' && rootFoldersTab) rootFoldersTab.focus();
|
||||||
|
if (tab === 'quality-profiles' && qualityProfilesTab) qualityProfilesTab.focus();
|
||||||
|
if (tab === 'monitor-settings' && monitorSettingsTab) monitorSettingsTab.focus();
|
||||||
|
}
|
||||||
|
const addOptionsStore = createLocalStorageStore<AddOptionsStore>('add-to-radarr-options', {
|
||||||
|
rootFolderPath: null,
|
||||||
|
qualityProfileId: null,
|
||||||
|
minimumAvailability: null
|
||||||
|
});
|
||||||
|
|
||||||
|
const sonarrOptions = Promise.all([
|
||||||
|
radarrApi.getRootFolders(),
|
||||||
|
radarrApi.getQualityProfiles()
|
||||||
|
]).then(([rootFolders, qualityProfiles]) => ({ rootFolders, qualityProfiles }));
|
||||||
|
|
||||||
|
sonarrOptions.then((s) => {
|
||||||
|
addOptionsStore.update((prev) => ({
|
||||||
|
rootFolderPath: prev.rootFolderPath || s.rootFolders[0]?.path || null,
|
||||||
|
qualityProfileId: prev.qualityProfileId || s.qualityProfiles[0]?.id || null,
|
||||||
|
minimumAvailability: prev.minimumAvailability || 'released'
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
addOptionsStore.subscribe(() => (tab = 'add-to-radarr'));
|
||||||
|
|
||||||
|
function handleAddToSonarr() {
|
||||||
|
return radarrApi
|
||||||
|
.addMovieToRadarr(tmdbId, {
|
||||||
|
rootFolderPath: $addOptionsStore.rootFolderPath || undefined,
|
||||||
|
qualityProfileId: $addOptionsStore.qualityProfileId || undefined,
|
||||||
|
minimumAvailability: $addOptionsStore.minimumAvailability || undefined
|
||||||
|
})
|
||||||
|
.then((success) => {
|
||||||
|
if (success) {
|
||||||
|
modalStack.close(modalId);
|
||||||
|
onComplete();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack(e: BackEvent) {
|
||||||
|
if (tab !== 'add-to-radarr') {
|
||||||
|
tab = 'add-to-radarr';
|
||||||
|
e.detail.stopPropagation();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabClasses = (active: boolean, secondary: boolean = false) =>
|
||||||
|
classNames('flex flex-col transition-all', {
|
||||||
|
'opacity-0 pointer-events-none': !active,
|
||||||
|
'-translate-x-10': !active && !secondary,
|
||||||
|
'translate-x-10': !active && secondary,
|
||||||
|
'absolute inset-0': secondary
|
||||||
|
});
|
||||||
|
|
||||||
|
const listItemClass = `flex items-center justify-between bg-primary-900 rounded-xl px-6 py-2.5 mb-4 font-medium
|
||||||
|
border-2 border-transparent focus:border-primary-500 hover:border-primary-500 cursor-pointer group`;
|
||||||
|
|
||||||
|
const scaledArrowClas = (hasFocus: boolean) =>
|
||||||
|
classNames('transition-transform', {
|
||||||
|
'text-primary-500 translate-x-0.5 scale-110': hasFocus,
|
||||||
|
'group-hover:text-primary-500 group-hover:translate-x-0.5 group-hover:scale-110': true
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<Dialog>
|
||||||
|
{#if backgroundUrl && tab === 'add-to-radarr'}
|
||||||
|
<div
|
||||||
|
transition:fade={{ duration: 200 }}
|
||||||
|
class="absolute inset-0 bg-cover bg-center h-52"
|
||||||
|
style="background-image: url({backgroundUrl}); -webkit-mask-image: radial-gradient(at 90% 10%, hsla(0,0%,0%,1) 0px, transparent 70%);"
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#await sonarrOptions then { qualityProfiles, rootFolders }}
|
||||||
|
{@const selectedRootFolder = rootFolders.find(
|
||||||
|
(f) => f.path === $addOptionsStore.rootFolderPath
|
||||||
|
)}
|
||||||
|
{@const selectedQualityProfile = qualityProfiles.find(
|
||||||
|
(f) => f.id === $addOptionsStore.qualityProfileId
|
||||||
|
)}
|
||||||
|
<Container on:back={handleBack} class="relative">
|
||||||
|
<Container
|
||||||
|
trapFocus
|
||||||
|
bind:selectable={addToSonarrTab}
|
||||||
|
class={tabClasses(tab === 'add-to-radarr')}
|
||||||
|
>
|
||||||
|
<div class="z-10 mb-8">
|
||||||
|
<div class="h-24" />
|
||||||
|
<h1 class="header2">Add {title} to Sonarr?</h1>
|
||||||
|
<div class="font-medium text-secondary-300 mb-8">
|
||||||
|
Before you can fetch episodes, you need to add this series to Sonarr.
|
||||||
|
</div>
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:clickOrSelect={() => (tab = 'root-folders')}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">Root Folder</h1>
|
||||||
|
{selectedRootFolder?.path}
|
||||||
|
({formatSize(selectedRootFolder?.freeSpace || 0)} left)
|
||||||
|
</div>
|
||||||
|
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:clickOrSelect={() => (tab = 'quality-profiles')}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">
|
||||||
|
Quality Profile
|
||||||
|
</h1>
|
||||||
|
<span>
|
||||||
|
{selectedQualityProfile?.name}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:clickOrSelect={() => (tab = 'monitor-settings')}
|
||||||
|
let:hasFocus
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<h1 class="text-secondary-300 font-semibold tracking-wide text-sm">
|
||||||
|
Minimum Availability
|
||||||
|
</h1>
|
||||||
|
<span>
|
||||||
|
{capitalize($addOptionsStore.minimumAvailability || 'released')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<ArrowRight class={scaledArrowClas(hasFocus)} size={24} />
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<!-- <Container class="flex items-center" on:clickOrSelect={() => (tab = 'quality-profiles')}>-->
|
||||||
|
<!-- {qualityProfile?.name}-->
|
||||||
|
<!-- <ArrowRight size={19} />-->
|
||||||
|
<!-- </Container>-->
|
||||||
|
<!-- <Container class="flex items-center" on:clickOrSelect={() => (tab = 'monitor-settings')}>-->
|
||||||
|
<!-- Monitor {$addOptionsStore.monitorSettings}-->
|
||||||
|
<!-- <ArrowRight size={19} />-->
|
||||||
|
<!-- </Container>-->
|
||||||
|
</div>
|
||||||
|
<Container class="flex flex-col space-y-4">
|
||||||
|
<Button type="primary-dark" action={handleAddToSonarr} focusOnMount>
|
||||||
|
<Plus size={19} slot="icon" />
|
||||||
|
Add to Radarr
|
||||||
|
</Button>
|
||||||
|
<Button type="primary-dark" on:clickOrSelect={() => modalStack.close(modalId)}>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
trapFocus
|
||||||
|
class={tabClasses(tab === 'root-folders', true)}
|
||||||
|
bind:selectable={rootFoldersTab}
|
||||||
|
>
|
||||||
|
<h1 class="text-xl text-secondary-100 font-medium mb-4">Root Folder</h1>
|
||||||
|
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||||
|
{#each rootFolders as rootFolder}
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:enter={scrollIntoView({ vertical: 64 })}
|
||||||
|
on:clickOrSelect={() =>
|
||||||
|
addOptionsStore.update((prev) => ({ ...prev, rootFolderId: rootFolder.id || 0 }))}
|
||||||
|
focusOnClick
|
||||||
|
focusOnMount={$addOptionsStore.rootFolderPath === rootFolder.path}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
{rootFolder.path} ({formatSize(rootFolder.freeSpace || 0)} left)
|
||||||
|
</div>
|
||||||
|
{#if selectedRootFolder?.id === rootFolder.id}
|
||||||
|
<Check size={24} />
|
||||||
|
{/if}
|
||||||
|
</Container>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
trapFocus
|
||||||
|
class={tabClasses(tab === 'quality-profiles', true)}
|
||||||
|
bind:selectable={qualityProfilesTab}
|
||||||
|
>
|
||||||
|
<h1 class="text-xl text-secondary-100 font-medium mb-4">Quality Profile</h1>
|
||||||
|
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||||
|
{#each qualityProfiles as qualityProfile}
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:enter={scrollIntoView({ vertical: 64 })}
|
||||||
|
on:clickOrSelect={() =>
|
||||||
|
addOptionsStore.update((prev) => ({
|
||||||
|
...prev,
|
||||||
|
qualityProfileId: qualityProfile.id || 0
|
||||||
|
}))}
|
||||||
|
focusOnClick
|
||||||
|
focusOnMount={$addOptionsStore.qualityProfileId === qualityProfile.id}
|
||||||
|
>
|
||||||
|
<div>{qualityProfile.name}</div>
|
||||||
|
{#if selectedQualityProfile?.id === qualityProfile.id}
|
||||||
|
<Check size={24} />
|
||||||
|
{/if}
|
||||||
|
</Container>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
|
||||||
|
<Container
|
||||||
|
trapFocus
|
||||||
|
class={tabClasses(tab === 'monitor-settings', true)}
|
||||||
|
bind:selectable={monitorSettingsTab}
|
||||||
|
>
|
||||||
|
<h1 class="text-xl text-secondary-100 font-medium mb-4">Monitor Episodes</h1>
|
||||||
|
<div class="min-h-0 overflow-y-auto scrollbar-hide">
|
||||||
|
{#each movieAvailabilities as availibility}
|
||||||
|
<Container
|
||||||
|
class={listItemClass}
|
||||||
|
on:enter={scrollIntoView({ vertical: 64 })}
|
||||||
|
on:clickOrSelect={() =>
|
||||||
|
addOptionsStore.update((prev) => ({ ...prev, monitorOptions: availibility }))}
|
||||||
|
focusOnClick
|
||||||
|
focusOnMount={$addOptionsStore.minimumAvailability === availibility}
|
||||||
|
>
|
||||||
|
<div>{capitalize(availibility)}</div>
|
||||||
|
{#if $addOptionsStore.minimumAvailability === availibility}
|
||||||
|
<Check size={24} />
|
||||||
|
{/if}
|
||||||
|
</Container>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
</Container>
|
||||||
|
{/await}
|
||||||
|
</Dialog>
|
||||||
@@ -5,34 +5,52 @@
|
|||||||
import ReleaseList from './Releases/MMReleasesTab.svelte';
|
import ReleaseList from './Releases/MMReleasesTab.svelte';
|
||||||
import DownloadList from '../MediaManager/DownloadList.svelte';
|
import DownloadList from '../MediaManager/DownloadList.svelte';
|
||||||
import FileList from './LocalFiles/MMLocalFilesTab.svelte';
|
import FileList from './LocalFiles/MMLocalFilesTab.svelte';
|
||||||
import { radarrApi } from '../../apis/radarr/radarr-api';
|
import { radarrApi, type RadarrMovie } from '../../apis/radarr/radarr-api';
|
||||||
|
import type { GrabReleaseFn } from './MediaManagerModal';
|
||||||
|
import type { Release } from '../../apis/combined-types';
|
||||||
|
import Dialog from '../Dialog/Dialog.svelte';
|
||||||
|
import MMReleasesTab from './Releases/MMReleasesTab.svelte';
|
||||||
|
|
||||||
|
export let radarrItem: RadarrMovie;
|
||||||
|
export let onGrabRelease: (release: Release) => void = () => {};
|
||||||
|
|
||||||
export let id: number; // Tmdb ID
|
|
||||||
export let modalId: symbol;
|
export let modalId: symbol;
|
||||||
export let hidden: boolean;
|
export let hidden: boolean;
|
||||||
|
|
||||||
const radarrItem = radarrApi.getMovieByTmdbId(id);
|
$: releases = radarrApi.getReleases(radarrItem.id || -1);
|
||||||
const downloads = radarrItem.then((i) => radarrApi.getDownloadsById(i?.id || -1));
|
|
||||||
const files = radarrItem.then((i) => radarrApi.getFilesByMovieId(i?.id || -1));
|
|
||||||
|
|
||||||
const getReleases = () => radarrItem.then((si) => radarrApi.getReleases(si?.id || -1));
|
const grabRelease: GrabReleaseFn = (release) =>
|
||||||
const selectRelease = () => {};
|
radarrApi.downloadMovie(release.guid || '', release.indexerId || -1).then((r) => {
|
||||||
|
onGrabRelease(release);
|
||||||
const cancelDownload = radarrApi.cancelDownloadRadarrMovie;
|
return r;
|
||||||
const handleSelectFile = () => {};
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MMModal {modalId} {hidden}>
|
<Dialog size="full" {modalId} {hidden}>
|
||||||
{#await radarrItem then movie}
|
<MMReleasesTab {releases} {grabRelease}>
|
||||||
{#if !movie}
|
<h1 slot="title">{radarrItem?.title}</h1>
|
||||||
<!-- <MMAddToSonarr />-->
|
<h2 slot="subtitle">
|
||||||
{:else}
|
Releases
|
||||||
<MMMainLayout>
|
<!--{#if season}-->
|
||||||
<h1 slot="title">{movie?.title}</h1>
|
<!-- Season {season} Releases-->
|
||||||
<ReleaseList slot="releases" {getReleases} {selectRelease} />
|
<!--{:else if 'episodeNumber' in sonarrItem}-->
|
||||||
<DownloadList slot="downloads" {downloads} {cancelDownload} />
|
<!-- Episode {sonarrItem.episodeNumber} Releases-->
|
||||||
<FileList slot="local-files" {files} {handleSelectFile} />
|
<!--{/if}-->
|
||||||
</MMMainLayout>
|
</h2>
|
||||||
{/if}
|
</MMReleasesTab>
|
||||||
{/await}
|
</Dialog>
|
||||||
</MMModal>
|
|
||||||
|
<!--<MMModal {modalId} {hidden}>-->
|
||||||
|
<!-- {#await radarrItem then movie}-->
|
||||||
|
<!-- {#if !movie}-->
|
||||||
|
<!-- <!– <MMAddToSonarr />–>-->
|
||||||
|
<!-- {:else}-->
|
||||||
|
<!-- <MMMainLayout>-->
|
||||||
|
<!-- <h1 slot="title">{movie?.title}</h1>-->
|
||||||
|
<!-- <ReleaseList slot="releases" {getReleases} {selectRelease} />-->
|
||||||
|
<!-- <DownloadList slot="downloads" {downloads} {cancelDownload} />-->
|
||||||
|
<!-- <FileList slot="local-files" {files} {handleSelectFile} />-->
|
||||||
|
<!-- </MMMainLayout>-->
|
||||||
|
<!-- {/if}-->
|
||||||
|
<!-- {/await}-->
|
||||||
|
<!--</MMModal>-->
|
||||||
|
|||||||
@@ -9,9 +9,10 @@
|
|||||||
|
|
||||||
export let season: number | undefined = undefined;
|
export let season: number | undefined = undefined;
|
||||||
export let sonarrItem: SonarrSeries | SonarrEpisode;
|
export let sonarrItem: SonarrSeries | SonarrEpisode;
|
||||||
|
export let onGrabRelease: (release: Release) => void = () => {};
|
||||||
|
|
||||||
export let modalId: symbol;
|
export let modalId: symbol;
|
||||||
export let hidden: boolean;
|
export let hidden: boolean;
|
||||||
export let onGrabRelease: (release: Release) => void = () => {};
|
|
||||||
|
|
||||||
$: releases = getReleases(season);
|
$: releases = getReleases(season);
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@
|
|||||||
import { formatSize } from '../../utils';
|
import { formatSize } from '../../utils';
|
||||||
import { Cross1 } from 'radix-icons-svelte';
|
import { Cross1 } from 'radix-icons-svelte';
|
||||||
import { capitalize } from '../../utils.js';
|
import { capitalize } from '../../utils.js';
|
||||||
|
import type { Download } from '../../apis/combined-types';
|
||||||
|
|
||||||
export let download: EpisodeDownload;
|
export let download: Download;
|
||||||
export let episode: SonarrEpisode | undefined;
|
export let title: string;
|
||||||
|
export let subtitle: string;
|
||||||
|
export let backgroundUrl: string;
|
||||||
export let onCancel: () => void;
|
export let onCancel: () => void;
|
||||||
$: backgroundUrl = episode?.images?.[0]?.remoteUrl;
|
|
||||||
console.log(download);
|
|
||||||
|
|
||||||
function handleCancelDownload() {
|
function handleCancelDownload() {
|
||||||
return sonarrApi.cancelDownload(download.id || -1).then(() => onCancel());
|
return sonarrApi.cancelDownload(download.id || -1).then(() => onCancel());
|
||||||
@@ -33,8 +34,8 @@
|
|||||||
{#if backgroundUrl}
|
{#if backgroundUrl}
|
||||||
<div class="h-24" />
|
<div class="h-24" />
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="header2">{episode?.title}</h1>
|
<h1 class="header2">{title}</h1>
|
||||||
<h2 class="header1 mb-4">Season {episode?.seasonNumber} Episode {episode?.episodeNumber}</h2>
|
<h2 class="header1 mb-4">{subtitle}</h2>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
||||||
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
||||||
|
|||||||
@@ -1,19 +1,17 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Dialog from '../Dialog/Dialog.svelte';
|
import Dialog from '../Dialog/Dialog.svelte';
|
||||||
import {
|
import { sonarrApi } from '../../apis/sonarr/sonarr-api';
|
||||||
type EpisodeFileResource,
|
|
||||||
sonarrApi,
|
|
||||||
type SonarrEpisode
|
|
||||||
} from '../../apis/sonarr/sonarr-api';
|
|
||||||
import Button from '../Button.svelte';
|
import Button from '../Button.svelte';
|
||||||
import Container from '../../../Container.svelte';
|
import Container from '../../../Container.svelte';
|
||||||
import { formatSize } from '../../utils';
|
import { formatSize } from '../../utils';
|
||||||
import { Trash } from 'radix-icons-svelte';
|
import { Trash } from 'radix-icons-svelte';
|
||||||
|
import type { FileResource } from '../../apis/combined-types';
|
||||||
|
|
||||||
export let file: EpisodeFileResource;
|
export let file: FileResource;
|
||||||
export let episode: SonarrEpisode | undefined;
|
export let title = '';
|
||||||
|
export let subtitle = '';
|
||||||
|
export let backgroundUrl: string;
|
||||||
export let onDelete: () => void;
|
export let onDelete: () => void;
|
||||||
$: backgroundUrl = episode?.images?.[0]?.remoteUrl;
|
|
||||||
|
|
||||||
function handleDeleteFile() {
|
function handleDeleteFile() {
|
||||||
return sonarrApi.deleteSonarrEpisode(file.id || -1).then(() => onDelete());
|
return sonarrApi.deleteSonarrEpisode(file.id || -1).then(() => onDelete());
|
||||||
@@ -31,8 +29,8 @@
|
|||||||
{#if backgroundUrl}
|
{#if backgroundUrl}
|
||||||
<div class="h-24" />
|
<div class="h-24" />
|
||||||
{/if}
|
{/if}
|
||||||
<h1 class="header2">{episode?.title}</h1>
|
<h1 class="header2">{title}</h1>
|
||||||
<h2 class="header1 mb-4">Season {episode?.seasonNumber} Episode {episode?.episodeNumber}</h2>
|
<h2 class="header1 mb-4">{subtitle}</h2>
|
||||||
<div
|
<div
|
||||||
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
class="grid grid-cols-[1fr_max-content] font-medium mb-16
|
||||||
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
[&>*:nth-child(odd)]:text-secondary-300 [&>*:nth-child(even)]:text-right [&>*:nth-child(even)]:text-secondary-100 *:py-1"
|
||||||
|
|||||||
@@ -347,13 +347,17 @@
|
|||||||
if (file)
|
if (file)
|
||||||
modalStack.create(FileDetailsDialog, {
|
modalStack.create(FileDetailsDialog, {
|
||||||
file,
|
file,
|
||||||
episode,
|
title: episode?.title || '',
|
||||||
|
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
|
||||||
|
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
|
||||||
onDelete: () => (sonarrFiles = getFiles(sonarrItem))
|
onDelete: () => (sonarrFiles = getFiles(sonarrItem))
|
||||||
});
|
});
|
||||||
else if (download)
|
else if (download)
|
||||||
modalStack.create(DownloadDetailsDialog, {
|
modalStack.create(DownloadDetailsDialog, {
|
||||||
download,
|
download,
|
||||||
episode,
|
title: episode?.title || '',
|
||||||
|
subtitle: `Season ${episode?.seasonNumber} Episode ${episode?.episodeNumber}`,
|
||||||
|
backgroundUrl: episode?.images?.[0]?.remoteUrl || '',
|
||||||
onCancel: () => (sonarrDownloads = getDownloads(sonarrItem))
|
onCancel: () => (sonarrDownloads = getDownloads(sonarrItem))
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -4,36 +4,112 @@
|
|||||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import { DotFilled, Download, ExternalLink, File, Play, Plus } from 'radix-icons-svelte';
|
import {
|
||||||
|
Cross1,
|
||||||
|
DotFilled,
|
||||||
|
Download,
|
||||||
|
ExternalLink,
|
||||||
|
File,
|
||||||
|
Play,
|
||||||
|
Plus,
|
||||||
|
Trash
|
||||||
|
} from 'radix-icons-svelte';
|
||||||
import Button from '../components/Button.svelte';
|
import Button from '../components/Button.svelte';
|
||||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||||
import { radarrApi } from '../apis/radarr/radarr-api';
|
import { type MovieDownload, type MovieFileResource, radarrApi } from '../apis/radarr/radarr-api';
|
||||||
import { useActionRequests, useRequest } from '../stores/data.store';
|
import { useActionRequests, useRequest } from '../stores/data.store';
|
||||||
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
import DetachedPage from '../components/DetachedPage/DetachedPage.svelte';
|
||||||
import { openMovieMediaManager } from '../components/Modal/modal.store';
|
import { createModal, modalStack, openMovieMediaManager } from '../components/Modal/modal.store';
|
||||||
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
import { playerState } from '../components/VideoPlayer/VideoPlayer';
|
||||||
import { scrollIntoView } from '../selectable';
|
import { scrollIntoView } from '../selectable';
|
||||||
import Carousel from '../components/Carousel/Carousel.svelte';
|
import Carousel from '../components/Carousel/Carousel.svelte';
|
||||||
import TmdbPersonCard from '../components/PersonCard/TmdbPersonCard.svelte';
|
import TmdbPersonCard from '../components/PersonCard/TmdbPersonCard.svelte';
|
||||||
import TmdbCard from '../components/Card/TmdbCard.svelte';
|
import TmdbCard from '../components/Card/TmdbCard.svelte';
|
||||||
|
import MovieMediaManagerModal from '../components/MediaManagerModal/MovieMediaManagerModal.svelte';
|
||||||
|
import MMAddToRadarrDialog from '../components/MediaManagerModal/MMAddToRadarrDialog.svelte';
|
||||||
|
import FileDetailsDialog from '../components/SeriesPage/FileDetailsDialog.svelte';
|
||||||
|
import DownloadDetailsDialog from '../components/SeriesPage/DownloadDetailsDialog.svelte';
|
||||||
|
import { capitalize, formatSize } from '../utils';
|
||||||
|
import ConfirmDialog from '../components/Dialog/ConfirmDialog.svelte';
|
||||||
|
import { TMDB_BACKDROP_SMALL } from '../constants.js';
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
|
const tmdbId = Number(id);
|
||||||
|
|
||||||
const { promise: movieDataP } = useRequest(tmdbApi.getTmdbMovie, Number(id));
|
const tmdbMovie = tmdbApi.getTmdbMovie(tmdbId);
|
||||||
$: recommendations = tmdbApi.getMovieRecommendations(Number(id));
|
$: recommendations = tmdbApi.getMovieRecommendations(tmdbId);
|
||||||
const { promise: jellyfinItemP } = useRequest(
|
const { promise: jellyfinItemP } = useRequest(
|
||||||
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
|
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
|
||||||
id
|
id
|
||||||
);
|
);
|
||||||
const { promise: radarrItemP, send: refreshRadarrItem } = useRequest(
|
const { promise: radarrItemP, send: refreshRadarrItem } = useRequest(
|
||||||
radarrApi.getMovieByTmdbId,
|
radarrApi.getMovieByTmdbId,
|
||||||
Number(id)
|
tmdbId
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let radarrItem = radarrApi.getMovieByTmdbId(tmdbId);
|
||||||
|
$: radarrDownloads = getDownloads(radarrItem);
|
||||||
|
$: radarrFiles = getFiles(radarrItem);
|
||||||
|
|
||||||
const { requests, isFetching, data } = useActionRequests({
|
const { requests, isFetching, data } = useActionRequests({
|
||||||
handleAddToRadarr: (id: number) =>
|
handleAddToRadarr: (id: number) =>
|
||||||
radarrApi.addMovieToRadarr(id).finally(() => refreshRadarrItem(Number(id)))
|
radarrApi.addMovieToRadarr(id).finally(() => refreshRadarrItem(tmdbId))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function getFiles(item: typeof radarrItem) {
|
||||||
|
return item.then((item) => (item ? radarrApi.getFilesByMovieId(item?.id || -1) : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getDownloads(item: typeof radarrItem) {
|
||||||
|
return item.then((item) => (item ? radarrApi.getDownloadsById(item?.id || -1) : []));
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleAddedToRadarr() {
|
||||||
|
radarrItem = radarrApi.getMovieByTmdbId(tmdbId);
|
||||||
|
radarrItem.then(
|
||||||
|
(radarrItem) =>
|
||||||
|
radarrItem && createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease })
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const onGrabRelease = () => setTimeout(() => (radarrDownloads = getDownloads(radarrItem)), 8000);
|
||||||
|
|
||||||
|
async function handleRequest() {
|
||||||
|
return radarrItem.then((radarrItem) => {
|
||||||
|
if (radarrItem) createModal(MovieMediaManagerModal, { radarrItem, onGrabRelease });
|
||||||
|
else
|
||||||
|
return tmdbMovie.then((tmdbMovie) => {
|
||||||
|
createModal(MMAddToRadarrDialog, {
|
||||||
|
title: tmdbMovie?.title || '',
|
||||||
|
tmdbId,
|
||||||
|
backdropUri: tmdbMovie?.backdrop_path || '',
|
||||||
|
onComplete: handleAddedToRadarr
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfirmDeleteSeasonDialog(files: MovieFileResource[]) {
|
||||||
|
createModal(ConfirmDialog, {
|
||||||
|
header: 'Delete Season Files?',
|
||||||
|
body: `Are you sure you want to delete all ${files.length} file(s)?`, // TODO: These messages could be better, for series too
|
||||||
|
confirm: () =>
|
||||||
|
radarrApi
|
||||||
|
.deleteFiles(files.map((f) => f.id || -1))
|
||||||
|
.then(() => (radarrFiles = getFiles(radarrItem)))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function createConfirmCancelDownloadsDialog(downloads: MovieDownload[]) {
|
||||||
|
createModal(ConfirmDialog, {
|
||||||
|
header: 'Cancel Season Downloads?',
|
||||||
|
body: `Are you sure you want to cancel all ${downloads.length} download(s)?`, // TODO: These messages could be better, for series too
|
||||||
|
confirm: () =>
|
||||||
|
radarrApi
|
||||||
|
.cancelDownloads(downloads.map((f) => f.id || -1))
|
||||||
|
.then(() => (radarrDownloads = getDownloads(radarrItem)))
|
||||||
|
});
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<DetachedPage let:handleGoBack let:registrar>
|
<DetachedPage let:handleGoBack let:registrar>
|
||||||
@@ -43,7 +119,7 @@
|
|||||||
on:enter={scrollIntoView({ top: 999 })}
|
on:enter={scrollIntoView({ top: 999 })}
|
||||||
>
|
>
|
||||||
<HeroCarousel
|
<HeroCarousel
|
||||||
urls={$movieDataP.then(
|
urls={tmdbMovie.then(
|
||||||
(movie) =>
|
(movie) =>
|
||||||
movie?.images.backdrops
|
movie?.images.backdrops
|
||||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||||
@@ -53,7 +129,7 @@
|
|||||||
>
|
>
|
||||||
<Container />
|
<Container />
|
||||||
<div class="h-full flex-1 flex flex-col justify-end">
|
<div class="h-full flex-1 flex flex-col justify-end">
|
||||||
{#await $movieDataP then movie}
|
{#await tmdbMovie then movie}
|
||||||
{#if movie}
|
{#if movie}
|
||||||
<div
|
<div
|
||||||
class={classNames(
|
class={classNames(
|
||||||
@@ -104,25 +180,29 @@
|
|||||||
<Play size={19} slot="icon" />
|
<Play size={19} slot="icon" />
|
||||||
</Button>
|
</Button>
|
||||||
{/if}
|
{/if}
|
||||||
{#if radarrItem}
|
<Button class="mr-4" action={handleRequest}>
|
||||||
<Button class="mr-4" on:clickOrSelect={() => openMovieMediaManager(Number(id))}>
|
Request
|
||||||
{#if jellyfinItem}
|
<Plus size={19} slot="icon" />
|
||||||
Manage Media
|
</Button>
|
||||||
{:else}
|
<!--{#if radarrItem}-->
|
||||||
Request
|
<!-- <Button class="mr-4" on:clickOrSelect={() => openMovieMediaManager(Number(id))}>-->
|
||||||
{/if}
|
<!-- {#if jellyfinItem}-->
|
||||||
<svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />
|
<!-- Manage Media-->
|
||||||
</Button>
|
<!-- {:else}-->
|
||||||
{:else}
|
<!-- Request-->
|
||||||
<Button
|
<!-- {/if}-->
|
||||||
class="mr-4"
|
<!-- <svelte:component this={jellyfinItem ? File : Download} size={19} slot="icon" />-->
|
||||||
on:clickOrSelect={() => requests.handleAddToRadarr(Number(id))}
|
<!-- </Button>-->
|
||||||
disabled={$isFetching.handleAddToRadarr}
|
<!--{:else}-->
|
||||||
>
|
<!-- <Button-->
|
||||||
Add to Radarr
|
<!-- class="mr-4"-->
|
||||||
<Plus slot="icon" size={19} />
|
<!-- on:clickOrSelect={() => requests.handleAddToRadarr(Number(id))}-->
|
||||||
</Button>
|
<!-- disabled={$isFetching.handleAddToRadarr}-->
|
||||||
{/if}
|
<!-- >-->
|
||||||
|
<!-- Add to Radarr-->
|
||||||
|
<!-- <Plus slot="icon" size={19} />-->
|
||||||
|
<!-- </Button>-->
|
||||||
|
<!--{/if}-->
|
||||||
{#if PLATFORM_WEB}
|
{#if PLATFORM_WEB}
|
||||||
<Button class="mr-4">
|
<Button class="mr-4">
|
||||||
Open In TMDB
|
Open In TMDB
|
||||||
@@ -139,7 +219,7 @@
|
|||||||
</HeroCarousel>
|
</HeroCarousel>
|
||||||
</Container>
|
</Container>
|
||||||
<Container on:enter={scrollIntoView({ top: 0 })} class="">
|
<Container on:enter={scrollIntoView({ top: 0 })} class="">
|
||||||
{#await $movieDataP then movie}
|
{#await tmdbMovie then movie}
|
||||||
<Carousel scrollClass="px-32" class="mb-8">
|
<Carousel scrollClass="px-32" class="mb-8">
|
||||||
<div slot="header">Show Cast</div>
|
<div slot="header">Show Cast</div>
|
||||||
{#each movie?.credits?.cast?.slice(0, 15) || [] as credit}
|
{#each movie?.credits?.cast?.slice(0, 15) || [] as credit}
|
||||||
@@ -156,7 +236,7 @@
|
|||||||
</Carousel>
|
</Carousel>
|
||||||
{/await}
|
{/await}
|
||||||
</Container>
|
</Container>
|
||||||
{#await $movieDataP then movie}
|
{#await tmdbMovie then movie}
|
||||||
<Container class="flex-1 bg-secondary-950 pt-8 px-32" on:enter={scrollIntoView({ top: 0 })}>
|
<Container class="flex-1 bg-secondary-950 pt-8 px-32" on:enter={scrollIntoView({ top: 0 })}>
|
||||||
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">More Information</h1>
|
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">More Information</h1>
|
||||||
<div class="text-zinc-300 font-medium text-lg flex flex-wrap">
|
<div class="text-zinc-300 font-medium text-lg flex flex-wrap">
|
||||||
@@ -201,5 +281,115 @@
|
|||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
{/await}
|
{/await}
|
||||||
|
{#await Promise.all([tmdbMovie, radarrFiles, radarrDownloads]) then [movie, files, downloads]}
|
||||||
|
{#if files?.length || downloads?.length}
|
||||||
|
<Container
|
||||||
|
class="flex-1 bg-secondary-950 pt-8 pb-16 px-32 flex flex-col"
|
||||||
|
on:enter={scrollIntoView({ top: 32 })}
|
||||||
|
>
|
||||||
|
<h1 class="font-medium tracking-wide text-2xl text-zinc-300 mb-8">Local Files</h1>
|
||||||
|
<div class="space-y-8">
|
||||||
|
<Container direction="grid" gridCols={2} class="grid grid-cols-2 gap-8">
|
||||||
|
{#each downloads as download}
|
||||||
|
<Container
|
||||||
|
class={classNames(
|
||||||
|
'flex space-x-8 items-center text-zinc-300 font-medium relative overflow-hidden',
|
||||||
|
'px-8 py-4 border-2 border-transparent rounded-xl',
|
||||||
|
{
|
||||||
|
'bg-secondary-800 focus-within:bg-primary-700 focus-within:border-primary-500': true,
|
||||||
|
'hover:bg-primary-700 hover:border-primary-500 cursor-pointer': true
|
||||||
|
// 'bg-primary-700 focus-within:border-primary-500': selected,
|
||||||
|
// 'bg-secondary-800 focus-within:border-zinc-300': !selected
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
on:clickOrSelect={() =>
|
||||||
|
modalStack.create(DownloadDetailsDialog, {
|
||||||
|
download,
|
||||||
|
title: movie?.title || '',
|
||||||
|
subtitle: download.title || '',
|
||||||
|
backgroundUrl: TMDB_BACKDROP_SMALL + movie?.backdrop_path || '',
|
||||||
|
onCancel: () => (radarrDownloads = getDownloads(radarrItem))
|
||||||
|
})}
|
||||||
|
on:enter={scrollIntoView({ vertical: 128 })}
|
||||||
|
focusOnClick
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="absolute inset-0 bg-secondary-50/10"
|
||||||
|
style={`width: ${
|
||||||
|
(((download.size || 0) - (download.sizeleft || 0)) / (download.size || 1)) *
|
||||||
|
100
|
||||||
|
}%`}
|
||||||
|
/>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-lg">
|
||||||
|
{capitalize(download.status || movie?.title || '')}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
{formatSize((download?.size || 0) - (download?.sizeleft || 1))} / {formatSize(
|
||||||
|
download?.size || 0
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{download?.quality?.quality?.name}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
{/each}
|
||||||
|
{#each files as file}
|
||||||
|
<Container
|
||||||
|
class={classNames(
|
||||||
|
'flex space-x-8 items-center text-zinc-300 font-medium relative overflow-hidden',
|
||||||
|
'px-8 py-4 border-2 border-transparent rounded-xl',
|
||||||
|
{
|
||||||
|
'bg-secondary-800 focus-within:bg-primary-700 focus-within:border-primary-500': true,
|
||||||
|
'hover:bg-primary-700 hover:border-primary-500 cursor-pointer': true
|
||||||
|
// 'bg-primary-700 focus-within:border-primary-500': selected,
|
||||||
|
// 'bg-secondary-800 focus-within:border-zinc-300': !selected
|
||||||
|
}
|
||||||
|
)}
|
||||||
|
on:clickOrSelect={() =>
|
||||||
|
modalStack.create(FileDetailsDialog, {
|
||||||
|
file,
|
||||||
|
title: movie?.title || '',
|
||||||
|
subtitle: file.relativePath || '',
|
||||||
|
backgroundUrl: TMDB_BACKDROP_SMALL + movie?.backdrop_path || '',
|
||||||
|
onDelete: () => (radarrFiles = getFiles(radarrItem))
|
||||||
|
})}
|
||||||
|
on:enter={scrollIntoView({ vertical: 128 })}
|
||||||
|
focusOnClick
|
||||||
|
>
|
||||||
|
<div class="flex-1">
|
||||||
|
<h1 class="text-lg">
|
||||||
|
{file?.quality?.quality?.name}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{file?.mediaInfo?.runTime}
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
{formatSize(file?.size || 0)}
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
{/each}
|
||||||
|
</Container>
|
||||||
|
<Container direction="horizontal" class="flex mt-0">
|
||||||
|
{#if files?.length}
|
||||||
|
<Button on:clickOrSelect={() => createConfirmDeleteSeasonDialog(files)}>
|
||||||
|
<Trash size={19} slot="icon" />
|
||||||
|
Delete All Files
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
{#if downloads?.length}
|
||||||
|
<Button on:clickOrSelect={() => createConfirmCancelDownloadsDialog(downloads)}>
|
||||||
|
<Cross1 size={19} slot="icon" />
|
||||||
|
Cancel All Downloads
|
||||||
|
</Button>
|
||||||
|
{/if}
|
||||||
|
</Container>
|
||||||
|
</div>
|
||||||
|
</Container>
|
||||||
|
{/if}
|
||||||
|
{/await}
|
||||||
</div>
|
</div>
|
||||||
</DetachedPage>
|
</DetachedPage>
|
||||||
|
|||||||
Reference in New Issue
Block a user