diff --git a/src/App.svelte b/src/App.svelte index d5ff876..3b32fcb 100644 --- a/src/App.svelte +++ b/src/App.svelte @@ -47,7 +47,7 @@ - + diff --git a/src/Container.svelte b/src/Container.svelte index 5404674..5eed54b 100644 --- a/src/Container.svelte +++ b/src/Container.svelte @@ -72,6 +72,7 @@ rest.container._initializeSelectable(); if (focusOnMount) { + console.log('focusing', rest.container.getHtmlElement()); rest.container.focus(); } diff --git a/src/lib/apis/combined-types.d.ts b/src/lib/apis/combined-types.d.ts new file mode 100644 index 0000000..2131498 --- /dev/null +++ b/src/lib/apis/combined-types.d.ts @@ -0,0 +1,6 @@ +import type { MovieDownload, MovieFileResource, RadarrRelease } from './radarr/radarr-api'; +import type { EpisodeFileResource, EpisodeDownload, SonarrRelease } from './sonarr/sonarr-api'; + +export type Release = RadarrRelease | SonarrRelease; +export type FileResource = MovieFileResource | EpisodeFileResource; +export type Download = MovieDownload | EpisodeDownload; diff --git a/src/lib/apis/radarr/radarr-api.ts b/src/lib/apis/radarr/radarr-api.ts index e91cf11..fcf1903 100644 --- a/src/lib/apis/radarr/radarr-api.ts +++ b/src/lib/apis/radarr/radarr-api.ts @@ -133,7 +133,7 @@ export class RadarrApi implements Api { }) .then((r) => r.data || []) || Promise.resolve([]); - downloadRadarrMovie = (guid: string, indexerId: number) => + downloadMovie = (guid: string, indexerId: number) => this.getClient() ?.POST('/api/v3/release', { params: {}, @@ -144,7 +144,7 @@ export class RadarrApi implements Api { }) .then((res) => res.response.ok) || Promise.resolve(false); - getMovieFilesByMovieId = (movieId: number): Promise => + getFilesByMovieId = (movieId: number): Promise => this.getClient() ?.GET('/api/v3/moviefile', { params: { @@ -154,7 +154,7 @@ export class RadarrApi implements Api { } }) .then((r) => r.data || []) || Promise.resolve([]); - deleteRadarrMovieFile = (id: number) => + deleteMovieFile = (id: number) => this.getClient() ?.DELETE('/api/v3/moviefile/{id}', { params: { @@ -177,7 +177,7 @@ export class RadarrApi implements Api { .then((r) => (r.data?.records?.filter((record) => record.movie) as MovieDownload[]) || []) || Promise.resolve([]); - getRadarrDownloadsById = (radarrId: number) => + getDownloadsById = (radarrId: number) => this.getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId)); getRadarrDownloadsByTmdbId = (tmdbId: number) => diff --git a/src/lib/apis/sonarr/sonarr-api.ts b/src/lib/apis/sonarr/sonarr-api.ts index a280347..f23f6fd 100644 --- a/src/lib/apis/sonarr/sonarr-api.ts +++ b/src/lib/apis/sonarr/sonarr-api.ts @@ -9,10 +9,12 @@ import { appState } from '../../stores/app-state.store'; import { createLocalStorageStore } from '../../stores/localstorage.store'; export type SonarrSeries = components['schemas']['SeriesResource']; +export type SonarrSeason = components['schemas']['SeasonResource']; export type SonarrRelease = components['schemas']['ReleaseResource']; -export type SeriesDownload = components['schemas']['QueueResource'] & { series: SonarrSeries }; +export type EpisodeDownload = components['schemas']['QueueResource'] & { series: SonarrSeries }; export type DiskSpaceInfo = components['schemas']['DiskSpaceResource']; export type SonarrEpisode = components['schemas']['EpisodeResource']; +export type EpisodeFileResource = components['schemas']['EpisodeFileResource']; export interface SonarrSeriesOptions { title: string; @@ -80,7 +82,18 @@ export class SonarrApi implements Api { return get(tmdbToTvdbCache)[tmdbId]; }; - getSonarrSeries = (): Promise => + getSeriesById = (id: number): Promise => + this.getClient() + ?.GET('/api/v3/series/{id}', { + params: { + path: { + id + } + } + }) + .then((r) => r.data) || Promise.resolve(undefined); + + getAllSeries = (): Promise => this.getClient() ?.GET('/api/v3/series', { params: {} @@ -177,7 +190,7 @@ export class SonarrApi implements Api { }) .then((res) => res.response.ok) || Promise.resolve(false); - getSonarrDownloads = (): Promise => + getSonarrDownloads = (): Promise => this.getClient() ?.GET('/api/v3/queue', { params: { @@ -191,7 +204,7 @@ export class SonarrApi implements Api { (r) => (r.data?.records?.filter( (record) => record.episode && record.series - ) as SeriesDownload[]) || [] + ) as EpisodeDownload[]) || [] ) || Promise.resolve([]); getSonarrDownloadsById = (sonarrId: number) => @@ -210,37 +223,48 @@ export class SonarrApi implements Api { }) .then((res) => res.response.ok) || Promise.resolve(false); - getSonarrEpisodes = async (seriesId: number) => { - const episodesPromise = - this.getClient() - ?.GET('/api/v3/episode', { - params: { - query: { - seriesId - } + getFilesBySeriesId = (seriesId: number): Promise => + this.getClient() + ?.GET('/api/v3/episodefile', { + params: { + query: { + seriesId } - }) - .then((r) => r.data || []) || Promise.resolve([]); + } + }) + .then((r) => r.data || []) || Promise.resolve([]); - const episodeFilesPromise = - this.getClient() - ?.GET('/api/v3/episodefile', { - params: { - query: { - seriesId - } - } - }) - .then((r) => r.data || []) || Promise.resolve([]); - - const episodes = await episodesPromise; - const episodeFiles = await episodeFilesPromise; - - return episodes.map((episode) => ({ - episode, - episodeFile: episodeFiles.find((file) => file.id === episode.episodeFileId) - })); - }; + // getSonarrEpisodes = async (seriesId: number) => { + // const episodesPromise = + // this.getClient() + // ?.GET('/api/v3/episode', { + // params: { + // query: { + // seriesId + // } + // } + // }) + // .then((r) => r.data || []) || Promise.resolve([]); + // + // const episodeFilesPromise = + // this.getClient() + // ?.GET('/api/v3/episodefile', { + // params: { + // query: { + // seriesId + // } + // } + // }) + // .then((r) => r.data || []) || Promise.resolve([]); + // + // const episodes = await episodesPromise; + // const episodeFiles = await episodeFilesPromise; + // + // return episodes.map((episode) => ({ + // episode, + // episodeFile: episodeFiles.find((file) => file.id === episode.episodeFileId) + // })); + // }; fetchSonarrReleases = async (episodeId: number) => this.getClient() @@ -265,13 +289,14 @@ export class SonarrApi implements Api { }) .then((r) => r.data || []) || Promise.resolve([]); - fetchSonarrEpisodes = async (seriesId: number): Promise => { + getEpisodes = async (seriesId: number, seasonNumber?: number): Promise => { return ( this.getClient() ?.GET('/api/v3/episode', { params: { query: { - seriesId + seriesId, + seasonNumber } } }) diff --git a/src/lib/components/Button.svelte b/src/lib/components/Button.svelte index 3dd1ace..723959f 100644 --- a/src/lib/components/Button.svelte +++ b/src/lib/components/Button.svelte @@ -16,7 +16,7 @@ { 'bg-highlight-foreground text-stone-900': $hasFoucus, 'hover:bg-highlight-foreground hover:text-stone-900': true, - 'bg-stone-800/50': !$hasFoucus, + 'bg-stone-800/90': !$hasFoucus, 'cursor-pointer': !inactive, 'cursor-not-allowed pointer-events-none opacity-40': inactive }, @@ -25,6 +25,7 @@ on:click on:select on:clickOrSelect + on:enter let:hasFocus {focusOnMount} > diff --git a/src/lib/components/ManageMedia/DownloadsList.svelte b/src/lib/components/ManageMedia/DownloadsList.svelte index 99db1b1..2fc0f64 100644 --- a/src/lib/components/ManageMedia/DownloadsList.svelte +++ b/src/lib/components/ManageMedia/DownloadsList.svelte @@ -4,8 +4,9 @@ import Button from '../Button.svelte'; import { formatSize } from '../../utils'; import { ChevronRight } from 'radix-icons-svelte'; + import type { Download } from '../../apis/combined-types'; - export let downloads: Promise; + export let downloads: Promise; export let cancelDownload: (downloadId: number) => Promise; diff --git a/src/lib/components/ManageMedia/LocalFiles/FileActionsModal.svelte b/src/lib/components/ManageMedia/LocalFiles/FileActionsModal.svelte index 72848a8..4eb00a2 100644 --- a/src/lib/components/ManageMedia/LocalFiles/FileActionsModal.svelte +++ b/src/lib/components/ManageMedia/LocalFiles/FileActionsModal.svelte @@ -5,9 +5,10 @@ import Button from '../../Button.svelte'; import FullScreenModal from '../../Modal/FullScreenModal.svelte'; import FullScreenModalContainer from '../ManageMediaMenuLayout.svelte'; + import type { FileResource } from '../../../apis/combined-types'; export let modalId: symbol; - export let file: MovieFileResource; + export let file: FileResource; export let handleDeleteFile: (fileId: number) => Promise; diff --git a/src/lib/components/ManageMedia/LocalFiles/FilesList.svelte b/src/lib/components/ManageMedia/LocalFiles/FilesList.svelte index 01ee4af..1b4204f 100644 --- a/src/lib/components/ManageMedia/LocalFiles/FilesList.svelte +++ b/src/lib/components/ManageMedia/LocalFiles/FilesList.svelte @@ -4,9 +4,12 @@ import Button from '../../Button.svelte'; import { ChevronRight } from 'radix-icons-svelte'; import { formatSize } from '../../../utils.js'; + import type { EpisodeFileResource } from '../../../apis/sonarr/sonarr-api'; + import type { FileResource } from '../../../apis/combined-types'; + import { scrollIntoView } from '../../../selectable'; - export let files: Promise; - export let handleSelectFile: (file: MovieFileResource) => void; + export let files: Promise; + export let handleSelectFile: (file: FileResource) => void;
@@ -18,23 +21,29 @@ {/each} {:then files} {#each files as file, index} -
- + + {:else}
No local files found
{/each} diff --git a/src/lib/components/ManageMedia/ManageMediaModal.svelte b/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte similarity index 81% rename from src/lib/components/ManageMedia/ManageMediaModal.svelte rename to src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte index 5264846..24086f7 100644 --- a/src/lib/components/ManageMedia/ManageMediaModal.svelte +++ b/src/lib/components/ManageMedia/RadarrMediaMangerModal.svelte @@ -14,24 +14,22 @@ import { useRequest } from '../../stores/data.store'; import { derived, type Readable } from 'svelte/store'; import ReleaseActionsModal from './Releases/ReleaseActionsModal.svelte'; + import type { SonarrRelease } from '../../apis/sonarr/sonarr-api'; export let modalId: symbol; export let hidden: boolean; export let id: number; - const { promise: files, refresh: refreshFiles } = useRequest( - radarrApi.getMovieFilesByMovieId, - id - ); + const { promise: files, refresh: refreshFiles } = useRequest(radarrApi.getFilesByMovieId, id); const { promise: downloads, data: downloadsData, refresh: refreshDownloads - } = useRequest(radarrApi.getRadarrDownloadsById, id); + } = useRequest(radarrApi.getDownloadsById, id); const handleGrabRelease = (guid: string, indexerId: number) => radarrApi - .downloadRadarrMovie(guid, indexerId) + .downloadMovie(guid, indexerId) .then((ok) => { if (!ok) { // TODO: Show error @@ -54,7 +52,7 @@ }, {}) ); - function handleSelectRelease(release: RadarrRelease) { + function handleSelectRelease(release: RadarrRelease | SonarrRelease) { modalStack.create( ReleaseActionsModal, { @@ -70,8 +68,7 @@ FileActionsModal, { file, - handleDeleteFile: (id: number) => - radarrApi.deleteRadarrMovieFile(id).then(() => refreshFiles(id)) + handleDeleteFile: (id: number) => radarrApi.deleteMovieFile(id).then(() => refreshFiles(id)) }, modalId ); @@ -81,7 +78,10 @@

Download

- + radarrApi.getReleases(id)} + selectRelease={handleSelectRelease} + />

Local Files

diff --git a/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte b/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte index 8b8f928..404536c 100644 --- a/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte +++ b/src/lib/components/ManageMedia/Releases/ReleaseActionsModal.svelte @@ -7,9 +7,11 @@ import FullScreenModalContainer from '../ManageMediaMenuLayout.svelte'; import { useActionRequest, useRequest } from '../../../stores/data.store'; import { Download, Plus } from 'radix-icons-svelte'; + import type { SonarrRelease } from '../../../apis/sonarr/sonarr-api'; + import type { Release } from '../../../apis/combined-types'; export let modalId: symbol; - export let release: RadarrRelease; + export let release: Release; export let status: undefined | 'downloading' | 'downloaded' = undefined; export let grabRelease: (guid: string, indexerId: number) => Promise; diff --git a/src/lib/components/ManageMedia/Releases/ReleaseList.svelte b/src/lib/components/ManageMedia/Releases/ReleaseList.svelte index af34b62..a6a5652 100644 --- a/src/lib/components/ManageMedia/Releases/ReleaseList.svelte +++ b/src/lib/components/ManageMedia/Releases/ReleaseList.svelte @@ -7,23 +7,25 @@ import { formatMinutesToTime, formatSize } from '../../../utils'; import { derived } from 'svelte/store'; import ButtonGhost from '../../Ghosts/ButtonGhost.svelte'; + import type { SonarrRelease } from '../../../apis/sonarr/sonarr-api'; - export let id: number; - export let getReleases: (id: number) => Promise; - export let selectRelease: (release: RadarrRelease) => void; + type Release = RadarrRelease | SonarrRelease; + + export let getReleases: () => Promise; + export let selectRelease: (release: Release) => void; let showAll = false; - const { data: releases, isLoading } = useRequest(getReleases, id); + const { data: releases, isLoading } = useRequest(getReleases); const filteredReleases = derived(releases, ($releases) => { if (!$releases) return []; let filtered = $releases.slice(); + const releaseIsEnough = (r: Release) => r?.quality?.quality?.resolution || 0 > 720; filtered.sort((a, b) => (b.seeders || 0) - (a.seeders || 0)); - filtered = (filtered as any) - .filter((release: any) => release?.quality?.quality?.resolution > 720) - .slice(0, 5); + filtered.sort((a, b) => (releaseIsEnough(b) ? 1 : 0) - (releaseIsEnough(a) ? 1 : 0)); + filtered = filtered.slice(0, 5); filtered.sort((a, b) => (b.size || 0) - (a.size || 0)); @@ -41,7 +43,11 @@ {:else} {#each (showAll ? $releases : $filteredReleases)?.filter((r) => r.guid && r.indexerId) || [] as release, index}
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
{/each} {#if !showAll && $releases?.length} diff --git a/src/lib/components/ManageMedia/Releases/ReleaseListModal.svelte b/src/lib/components/ManageMedia/Releases/ReleaseListModal.svelte new file mode 100644 index 0000000..e1d7bc5 --- /dev/null +++ b/src/lib/components/ManageMedia/Releases/ReleaseListModal.svelte @@ -0,0 +1,14 @@ + + + + + diff --git a/src/lib/components/ManageMedia/Releases/SeasonReleasesModal.svelte b/src/lib/components/ManageMedia/Releases/SeasonReleasesModal.svelte new file mode 100644 index 0000000..3592b80 --- /dev/null +++ b/src/lib/components/ManageMedia/Releases/SeasonReleasesModal.svelte @@ -0,0 +1,82 @@ + + + + + +

Episodes

+
+ {#await $episodes then episodes} + {#each episodes as episode} +
+ +
+ {/each} + {/await} +
+
+
diff --git a/src/lib/components/ManageMedia/SeasonList.svelte b/src/lib/components/ManageMedia/SeasonList.svelte new file mode 100644 index 0000000..41529ae --- /dev/null +++ b/src/lib/components/ManageMedia/SeasonList.svelte @@ -0,0 +1,33 @@ + + +
+ {#await $sonarrSeries then series} + {#if series?.seasons} + {#each series.seasons.filter((s) => s.seasonNumber !== 0) as season, i} +
+ +
+ {/each} + {/if} + {/await} +
diff --git a/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte b/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte new file mode 100644 index 0000000..cca6f51 --- /dev/null +++ b/src/lib/components/ManageMedia/SonarrMediaMangerModal.svelte @@ -0,0 +1,104 @@ + + + + +

Download

+ +
+ +

Local Files

+ +
+ +

Downloads

+ +
+
diff --git a/src/lib/components/Modal/FullScreenModal.svelte b/src/lib/components/Modal/FullScreenModal.svelte index 42b9628..e7f222f 100644 --- a/src/lib/components/Modal/FullScreenModal.svelte +++ b/src/lib/components/Modal/FullScreenModal.svelte @@ -19,6 +19,7 @@ class={classNames('fixed inset-0 bg-stone-950/80 overflow-auto', { 'opacity-0': hidden })} + canFocusEmpty >
diff --git a/src/lib/components/Modal/ModalStack.svelte b/src/lib/components/Modal/ModalStack.svelte index 76751fa..3b6a7f3 100644 --- a/src/lib/components/Modal/ModalStack.svelte +++ b/src/lib/components/Modal/ModalStack.svelte @@ -30,6 +30,13 @@ {@const hidden = $modalStackTop?.group === modal.group && $modalStackTop?.id !== modal.id}
- +
{/each} diff --git a/src/lib/components/Modal/modal.store.ts b/src/lib/components/Modal/modal.store.ts index f20a45e..dc8b382 100644 --- a/src/lib/components/Modal/modal.store.ts +++ b/src/lib/components/Modal/modal.store.ts @@ -21,7 +21,7 @@ function createModalStack() { function create

>( component: ComponentType>, - props: Omit, + props: Omit, group: symbol | undefined = undefined ) { const id = Symbol(); diff --git a/src/lib/components/SeriesPage/SeriesPage.svelte b/src/lib/components/SeriesPage/SeriesPage.svelte index 93f8a06..caae3c4 100644 --- a/src/lib/components/SeriesPage/SeriesPage.svelte +++ b/src/lib/components/SeriesPage/SeriesPage.svelte @@ -12,11 +12,12 @@ import Button from '../Button.svelte'; import { playerState } from '../VideoPlayer/VideoPlayer'; import { modalStack } from '../Modal/modal.store'; - import ManageMediaModal from '../ManageMedia/ManageMediaModal.svelte'; + import ManageMediaModal from '../ManageMedia/RadarrMediaMangerModal.svelte'; import { derived } from 'svelte/store'; import EpisodeCarousel from './EpisodeCarousel.svelte'; import { scrollIntoView, Selectable } from '../../selectable'; import ScrollHelper from '../ScrollHelper.svelte'; + import SonarrMediaMangerModal from '../ManageMedia/SonarrMediaMangerModal.svelte'; export let id: string; @@ -156,7 +157,7 @@