feat: Request utils
This commit is contained in:
@@ -23,8 +23,13 @@ module.exports = {
|
||||
files: ['*.svelte'],
|
||||
parser: 'svelte-eslint-parser',
|
||||
parserOptions: {
|
||||
parser: '@typescript-eslint/parser'
|
||||
parser: '@typescript-eslint/parser',
|
||||
}
|
||||
}
|
||||
]
|
||||
],
|
||||
rules: {
|
||||
'@typescript-eslint/ban-ts-comment': 'off',
|
||||
'@typescript-eslint/no-unused-vars': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
}
|
||||
};
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"build:tizen": "vite build --outDir tizen/dist",
|
||||
"build:tizen": "set VITE_PLATFORM=tv&& vite build --outDir tizen/dist",
|
||||
"preview": "vite preview",
|
||||
"preview:tizen": "vite build --outDir tizen/dist && vite preview --outDir tizen/dist",
|
||||
"deploy": "PORT=9494 NODE_ENV=production node build/",
|
||||
|
||||
@@ -190,7 +190,7 @@ export class JellyfinApi implements Api<paths> {
|
||||
}
|
||||
|
||||
export const jellyfinApi = new JellyfinApi();
|
||||
export const getJellyfinApiClient = jellyfinApi.getClient;
|
||||
export const jellyfinApiClient = jellyfinApi.getClient;
|
||||
|
||||
/*
|
||||
function getJellyfinApi() {
|
||||
|
||||
264
src/lib/apis/radarr/radarr-api.ts
Normal file
264
src/lib/apis/radarr/radarr-api.ts
Normal file
@@ -0,0 +1,264 @@
|
||||
import axios from 'axios';
|
||||
import createClient from 'openapi-fetch';
|
||||
import { get } from 'svelte/store';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
import type { components, paths } from './radarr.generated';
|
||||
import { getTmdbMovie } from '../tmdb/tmdb-api';
|
||||
import { log } from '../../utils';
|
||||
import { appState } from '../../stores/app-state.store';
|
||||
import type { Api } from '../api.interface';
|
||||
|
||||
export type RadarrMovie = components['schemas']['MovieResource'];
|
||||
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
||||
export type ReleaseResource = components['schemas']['ReleaseResource'];
|
||||
export type RadarrDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
|
||||
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
||||
|
||||
export interface RadarrMovieOptions {
|
||||
title: string;
|
||||
qualityProfileId: number;
|
||||
minimumAvailability: 'announced' | 'inCinemas' | 'released';
|
||||
tags: number[];
|
||||
profileId: number;
|
||||
year: number;
|
||||
rootFolderPath: string;
|
||||
tmdbId: number;
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
export class RadarrApi implements Api<paths> {
|
||||
getClient() {
|
||||
const radarrSettings = get(appState).user?.settings.radarr;
|
||||
const baseUrl = radarrSettings?.baseUrl;
|
||||
const apiKey = radarrSettings?.apiKey;
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl,
|
||||
headers: {
|
||||
'X-Api-Key': apiKey
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getBaseUrl() {
|
||||
return get(appState)?.user?.settings?.radarr.baseUrl || '';
|
||||
}
|
||||
|
||||
getSettings() {
|
||||
return get(appState).user?.settings.radarr;
|
||||
}
|
||||
|
||||
getMovieByTmdbId = (tmdbId: number): Promise<RadarrMovie | undefined> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/movie', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(tmdbId)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.find((m) => m.tmdbId == tmdbId)) || Promise.resolve(undefined);
|
||||
|
||||
getRadarrMovies = (): Promise<RadarrMovie[]> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/movie', {
|
||||
params: {}
|
||||
})
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
addMovieToRadarr = async (tmdbId: number) => {
|
||||
const tmdbMovie = await getTmdbMovie(tmdbId);
|
||||
const radarrMovie = await this.lookupRadarrMovieByTmdbId(tmdbId);
|
||||
|
||||
if (radarrMovie?.id) throw new Error('Movie already exists');
|
||||
|
||||
if (!tmdbMovie) throw new Error('Movie not found');
|
||||
|
||||
const options: RadarrMovieOptions = {
|
||||
qualityProfileId: get(appState).user?.settings.radarr.qualityProfileId || 0,
|
||||
profileId: get(appState).user?.settings.radarr?.qualityProfileId || 0,
|
||||
rootFolderPath: get(appState).user?.settings.radarr.rootFolderPath || '',
|
||||
minimumAvailability: 'announced',
|
||||
title: tmdbMovie.title || tmdbMovie.original_title || '',
|
||||
tmdbId: tmdbMovie.id || 0,
|
||||
year: Number(tmdbMovie.release_date?.slice(0, 4)),
|
||||
monitored: false,
|
||||
tags: [],
|
||||
searchNow: false
|
||||
};
|
||||
|
||||
return (
|
||||
this.getClient()
|
||||
?.POST('/api/v3/movie', {
|
||||
params: {},
|
||||
body: options
|
||||
})
|
||||
.then((r) => r.data) || Promise.resolve(undefined)
|
||||
);
|
||||
};
|
||||
|
||||
cancelDownloadRadarrMovie = async (downloadId: number) => {
|
||||
const deleteResponse = await this.getClient()
|
||||
?.DELETE('/api/v3/queue/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id: downloadId
|
||||
},
|
||||
query: {
|
||||
blocklist: false,
|
||||
removeFromClient: true
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => log(r));
|
||||
|
||||
return !!deleteResponse?.response.ok;
|
||||
};
|
||||
|
||||
fetchRadarrReleases = (movieId: number) =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/release', { params: { query: { movieId: movieId } } })
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
downloadRadarrMovie = (guid: string, indexerId: number) =>
|
||||
this.getClient()
|
||||
?.POST('/api/v3/release', {
|
||||
params: {},
|
||||
body: {
|
||||
indexerId,
|
||||
guid
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
deleteRadarrMovieFile = (id: number) =>
|
||||
this.getClient()
|
||||
?.DELETE('/api/v3/moviefile/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
getRadarrDownloads = (): Promise<RadarrDownload[]> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/queue', {
|
||||
params: {
|
||||
query: {
|
||||
includeMovie: true
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => (r.data?.records?.filter((record) => record.movie) as RadarrDownload[]) || []) ||
|
||||
Promise.resolve([]);
|
||||
|
||||
getRadarrDownloadsById = (radarrId: number) =>
|
||||
this.getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId));
|
||||
|
||||
getRadarrDownloadsByTmdbId = (tmdbId: number) =>
|
||||
this.getRadarrDownloads().then((downloads) =>
|
||||
downloads.filter((d) => d.movie.tmdbId === tmdbId)
|
||||
);
|
||||
|
||||
private lookupRadarrMovieByTmdbId = (tmdbId: number) =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/movie/lookup/tmdb', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as unknown as RadarrMovie) || Promise.resolve(undefined);
|
||||
|
||||
getDiskSpace = (): Promise<DiskSpaceInfo[]> =>
|
||||
this.getClient()
|
||||
?.GET('/api/v3/diskspace', {})
|
||||
.then((d) => d.data || []) || Promise.resolve([]);
|
||||
|
||||
removeFromRadarr = (id: number) =>
|
||||
this.getClient()
|
||||
?.DELETE('/api/v3/movie/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
getRadarrHealth = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get((baseUrl || this.getBaseUrl()) + '/api/v3/health', {
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || this.getSettings()?.apiKey
|
||||
}
|
||||
})
|
||||
.then((res) => res.status === 200)
|
||||
.catch(() => false);
|
||||
|
||||
getRootFolders = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get<components['schemas']['RootFolderResource'][]>(
|
||||
(baseUrl || this.getBaseUrl()) + '/api/v3/rootFolder',
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || this.getSettings()?.apiKey
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res) => res.data || []);
|
||||
|
||||
getQualityProfiles = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get<components['schemas']['QualityProfileResource'][]>(
|
||||
(baseUrl || get(appState)?.user?.settings.radarr.baseUrl) + '/api/v3/qualityprofile',
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || get(appState)?.user?.settings.radarr.apiKey
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res) => res.data || []);
|
||||
|
||||
getRadarrPosterUrl(item: RadarrMovie, original = false) {
|
||||
const url =
|
||||
get(settings).radarr.baseUrl +
|
||||
(item.images?.find((i) => i.coverType === 'poster')?.url || '');
|
||||
|
||||
if (!original) return url.replace('poster.jpg', `poster-${500}.jpg`);
|
||||
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
||||
export const radarrApi = new RadarrApi();
|
||||
export const radarrApiClient = radarrApi.getClient;
|
||||
|
||||
// function getRadarrApi() {
|
||||
// const baseUrl = get(settings)?.radarr.baseUrl;
|
||||
// const apiKey = get(settings)?.radarr.apiKey;
|
||||
// const rootFolder = get(settings)?.radarr.rootFolderPath;
|
||||
// const qualityProfileId = get(settings)?.radarr.qualityProfileId;
|
||||
//
|
||||
// if (!baseUrl || !apiKey || !rootFolder || !qualityProfileId) return undefined;
|
||||
//
|
||||
// return createClient<paths>({
|
||||
// baseUrl,
|
||||
// headers: {
|
||||
// 'X-Api-Key': apiKey
|
||||
// }
|
||||
// });
|
||||
// }
|
||||
@@ -1,233 +0,0 @@
|
||||
import axios from 'axios';
|
||||
import createClient from 'openapi-fetch';
|
||||
import { get } from 'svelte/store';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
import type { components, paths } from './radarr.generated';
|
||||
import { getTmdbMovie } from '../tmdb/tmdb-api';
|
||||
import { log } from '../../utils';
|
||||
|
||||
export type RadarrMovie = components['schemas']['MovieResource'];
|
||||
export type MovieFileResource = components['schemas']['MovieFileResource'];
|
||||
export type ReleaseResource = components['schemas']['ReleaseResource'];
|
||||
export type RadarrDownload = components['schemas']['QueueResource'] & { movie: RadarrMovie };
|
||||
export type DiskSpaceInfo = components['schemas']['DiskSpaceResource'];
|
||||
|
||||
export interface RadarrMovieOptions {
|
||||
title: string;
|
||||
qualityProfileId: number;
|
||||
minimumAvailability: 'announced' | 'inCinemas' | 'released';
|
||||
tags: number[];
|
||||
profileId: number;
|
||||
year: number;
|
||||
rootFolderPath: string;
|
||||
tmdbId: number;
|
||||
monitored?: boolean;
|
||||
searchNow?: boolean;
|
||||
}
|
||||
|
||||
function getRadarrApi() {
|
||||
const baseUrl = get(settings)?.radarr.baseUrl;
|
||||
const apiKey = get(settings)?.radarr.apiKey;
|
||||
const rootFolder = get(settings)?.radarr.rootFolderPath;
|
||||
const qualityProfileId = get(settings)?.radarr.qualityProfileId;
|
||||
|
||||
if (!baseUrl || !apiKey || !rootFolder || !qualityProfileId) return undefined;
|
||||
|
||||
return createClient<paths>({
|
||||
baseUrl,
|
||||
headers: {
|
||||
'X-Api-Key': apiKey
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export const getRadarrMovies = (): Promise<RadarrMovie[]> =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/movie', {
|
||||
params: {}
|
||||
})
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
export const getRadarrMovieByTmdbId = (tmdbId: string): Promise<RadarrMovie | undefined> =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/movie', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId: Number(tmdbId)
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data?.find((m) => (m.tmdbId as any) == tmdbId)) || Promise.resolve(undefined);
|
||||
|
||||
export const addMovieToRadarr = async (tmdbId: number) => {
|
||||
const tmdbMovie = await getTmdbMovie(tmdbId);
|
||||
const radarrMovie = await lookupRadarrMovieByTmdbId(tmdbId);
|
||||
|
||||
if (radarrMovie?.id) throw new Error('Movie already exists');
|
||||
|
||||
if (!tmdbMovie) throw new Error('Movie not found');
|
||||
|
||||
const options: RadarrMovieOptions = {
|
||||
qualityProfileId: get(settings)?.radarr.qualityProfileId || 0,
|
||||
profileId: get(settings)?.radarr.profileId || 0,
|
||||
rootFolderPath: get(settings)?.radarr.rootFolderPath || '',
|
||||
minimumAvailability: 'announced',
|
||||
title: tmdbMovie.title || tmdbMovie.original_title || '',
|
||||
tmdbId: tmdbMovie.id || 0,
|
||||
year: Number(tmdbMovie.release_date?.slice(0, 4)),
|
||||
monitored: false,
|
||||
tags: [],
|
||||
searchNow: false
|
||||
};
|
||||
|
||||
return (
|
||||
getRadarrApi()
|
||||
?.POST('/api/v3/movie', {
|
||||
params: {},
|
||||
body: options
|
||||
})
|
||||
.then((r) => r.data) || Promise.resolve(undefined)
|
||||
);
|
||||
};
|
||||
|
||||
export const cancelDownloadRadarrMovie = async (downloadId: number) => {
|
||||
const deleteResponse = await getRadarrApi()
|
||||
?.DELETE('/api/v3/queue/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id: downloadId
|
||||
},
|
||||
query: {
|
||||
blocklist: false,
|
||||
removeFromClient: true
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => log(r));
|
||||
|
||||
return !!deleteResponse?.response.ok;
|
||||
};
|
||||
|
||||
export const fetchRadarrReleases = (movieId: number) =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/release', { params: { query: { movieId: movieId } } })
|
||||
.then((r) => r.data || []) || Promise.resolve([]);
|
||||
|
||||
export const downloadRadarrMovie = (guid: string, indexerId: number) =>
|
||||
getRadarrApi()
|
||||
?.POST('/api/v3/release', {
|
||||
params: {},
|
||||
body: {
|
||||
indexerId,
|
||||
guid
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
export const deleteRadarrMovie = (id: number) =>
|
||||
getRadarrApi()
|
||||
?.DELETE('/api/v3/moviefile/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
export const getRadarrDownloads = (): Promise<RadarrDownload[]> =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/queue', {
|
||||
params: {
|
||||
query: {
|
||||
includeMovie: true
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => (r.data?.records?.filter((record) => record.movie) as RadarrDownload[]) || []) ||
|
||||
Promise.resolve([]);
|
||||
|
||||
export const getRadarrDownloadsById = (radarrId: number) =>
|
||||
getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.id === radarrId));
|
||||
|
||||
export const getRadarrDownloadsByTmdbId = (tmdbId: number) =>
|
||||
getRadarrDownloads().then((downloads) => downloads.filter((d) => d.movie.tmdbId === tmdbId));
|
||||
|
||||
const lookupRadarrMovieByTmdbId = (tmdbId: number) =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/movie/lookup/tmdb', {
|
||||
params: {
|
||||
query: {
|
||||
tmdbId
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((r) => r.data as any as RadarrMovie) || Promise.resolve(undefined);
|
||||
|
||||
export const getDiskSpace = (): Promise<DiskSpaceInfo[]> =>
|
||||
getRadarrApi()
|
||||
?.GET('/api/v3/diskspace', {})
|
||||
.then((d) => d.data || []) || Promise.resolve([]);
|
||||
|
||||
export const removeFromRadarr = (id: number) =>
|
||||
getRadarrApi()
|
||||
?.DELETE('/api/v3/movie/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id
|
||||
}
|
||||
}
|
||||
})
|
||||
.then((res) => res.response.ok) || Promise.resolve(false);
|
||||
|
||||
export const getRadarrHealth = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get((baseUrl || get(settings)?.radarr.baseUrl) + '/api/v3/health', {
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || get(settings)?.radarr.apiKey
|
||||
}
|
||||
})
|
||||
.then((res) => res.status === 200)
|
||||
.catch(() => false);
|
||||
|
||||
export const getRadarrRootFolders = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get<components['schemas']['RootFolderResource'][]>(
|
||||
(baseUrl || get(settings)?.sonarr.baseUrl) + '/api/v3/rootFolder',
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || get(settings)?.sonarr.apiKey
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res) => res.data || []);
|
||||
|
||||
export const getRadarrQualityProfiles = async (
|
||||
baseUrl: string | undefined = undefined,
|
||||
apiKey: string | undefined = undefined
|
||||
) =>
|
||||
axios
|
||||
.get<components['schemas']['QualityProfileResource'][]>(
|
||||
(baseUrl || get(settings)?.sonarr.baseUrl) + '/api/v3/qualityprofile',
|
||||
{
|
||||
headers: {
|
||||
'X-Api-Key': apiKey || get(settings)?.sonarr.apiKey
|
||||
}
|
||||
}
|
||||
)
|
||||
.then((res) => res.data || []);
|
||||
|
||||
export function getRadarrPosterUrl(item: RadarrMovie, original = false) {
|
||||
const url =
|
||||
get(settings).radarr.baseUrl + (item.images?.find((i) => i.coverType === 'poster')?.url || '');
|
||||
|
||||
if (!original) return url.replace('poster.jpg', `poster-${500}.jpg`);
|
||||
|
||||
return url;
|
||||
}
|
||||
@@ -2,16 +2,32 @@
|
||||
import Container from '../../Container.svelte';
|
||||
import type { Readable } from 'svelte/store';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export let inactive: boolean = false;
|
||||
|
||||
let hasFoucus: Readable<boolean>;
|
||||
</script>
|
||||
|
||||
<Container
|
||||
bind:hasFocus={hasFoucus}
|
||||
class={classNames('selectable px-6 py-2 rounded-xl font-medium tracking-wide', {
|
||||
class={classNames('px-6 py-2 rounded-lg font-medium tracking-wide flex items-center', {
|
||||
'bg-stone-200 text-stone-900': $hasFoucus,
|
||||
'bg-stone-800 text-white': !$hasFoucus
|
||||
'hover:bg-stone-200 hover:text-stone-900': true,
|
||||
'bg-stone-800/50': !$hasFoucus,
|
||||
'cursor-pointer': !inactive,
|
||||
'cursor-not-allowed pointer-events-none opacity-40': inactive
|
||||
})}
|
||||
on:click
|
||||
>
|
||||
{#if $$slots.icon}
|
||||
<div class="mr-2">
|
||||
<slot name="icon" />
|
||||
</div>
|
||||
{/if}
|
||||
<slot />
|
||||
{#if $$slots['icon-after']}
|
||||
<div class="ml-2">
|
||||
<slot name="icon-after" />
|
||||
</div>
|
||||
{/if}
|
||||
</Container>
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
setJellyfinItemWatched,
|
||||
type JellyfinItem
|
||||
} from '$lib/apis/jellyfin/jellyfinApi';
|
||||
import type { RadarrMovie } from '../../apis/radarr/radarrApi';
|
||||
import type { RadarrMovie } from '../../apis/radarr/radarr-api';
|
||||
import type { SonarrSeries } from '../../apis/sonarr/sonarrApi';
|
||||
import { jellyfinItemsStore } from '../../stores/data.store';
|
||||
import { settings } from '../../stores/settings.store';
|
||||
|
||||
0
src/lib/components/DetatchedPage.svelte
Normal file
0
src/lib/components/DetatchedPage.svelte
Normal file
@@ -40,7 +40,7 @@
|
||||
<Laptop class="w-8 h-8" slot="icon" />
|
||||
</div>
|
||||
</Container>
|
||||
<Container on:click={() => navigate('/movie/695721')}>
|
||||
<Container on:click={() => navigate('/movie/359410')}>
|
||||
<div class={itemContainer(1, $focusIndex)}>
|
||||
<CardStack class="w-8 h-8" slot="icon" />
|
||||
</div>
|
||||
|
||||
@@ -6,3 +6,6 @@ export const TMDB_POSTER_SMALL = 'https://www.themoviedb.org/t/p/w342';
|
||||
export const TMDB_PROFILE_SMALL = 'https://www.themoviedb.org/t/p/w185';
|
||||
|
||||
export const PLACEHOLDER_BACKDROP = '/plcaeholder.jpg';
|
||||
|
||||
export const PLATFORM_TV: boolean = import.meta.env.VITE_PLATFORM === 'tv';
|
||||
export const PLATFORM_WEB: boolean = !PLATFORM_TV;
|
||||
|
||||
@@ -2,28 +2,42 @@
|
||||
import Container from '../../Container.svelte';
|
||||
import HeroCarousel from '../components/HeroCarousel/HeroCarousel.svelte';
|
||||
import { tmdbApi } from '../apis/tmdb/tmdb-api';
|
||||
import { TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import { PLATFORM_WEB, TMDB_IMAGES_ORIGINAL } from '../constants';
|
||||
import classNames from 'classnames';
|
||||
import { DotFilled } from 'radix-icons-svelte';
|
||||
import { DotFilled, ExternalLink, Plus } from 'radix-icons-svelte';
|
||||
import Button from '../components/Button.svelte';
|
||||
import { jellyfinApi } from '../apis/jellyfin/jellyfin-api';
|
||||
import VideoPlayer from '../components/VideoPlayer/VideoPlayer.svelte';
|
||||
import { radarrApi } from '../apis/radarr/radarr-api';
|
||||
import { useActionRequests, useRequest } from '../stores/data.store';
|
||||
|
||||
export let id: string;
|
||||
|
||||
const movieDataP = tmdbApi.getTmdbMovie(Number(id));
|
||||
const jellyfinItem = jellyfinApi.getLibraryItemFromTmdbId(id);
|
||||
const { promise: movieDataP } = useRequest(tmdbApi.getTmdbMovie, Number(id));
|
||||
const { promise: jellyfinItemP } = useRequest(
|
||||
(id: string) => jellyfinApi.getLibraryItemFromTmdbId(id),
|
||||
id
|
||||
);
|
||||
const { promise: radarrItemP, refresh: refreshRadarrItem } = useRequest(
|
||||
radarrApi.getMovieByTmdbId,
|
||||
Number(id)
|
||||
);
|
||||
|
||||
let playbackId: string = '';
|
||||
|
||||
let heroIndex: number;
|
||||
|
||||
const { requests, isFetching, data } = useActionRequests({
|
||||
handleAddToRadarr: (id: number) =>
|
||||
radarrApi.addMovieToRadarr(id).finally(() => refreshRadarrItem(Number(id)))
|
||||
});
|
||||
</script>
|
||||
|
||||
<Container focusOnMount>
|
||||
<div class="h-screen flex flex-col">
|
||||
<HeroCarousel
|
||||
bind:index={heroIndex}
|
||||
urls={movieDataP.then(
|
||||
urls={$movieDataP.then(
|
||||
(movie) =>
|
||||
movie?.images.backdrops
|
||||
?.sort((a, b) => (b.vote_count || 0) - (a.vote_count || 0))
|
||||
@@ -32,7 +46,7 @@
|
||||
)}
|
||||
>
|
||||
<div class="h-full flex flex-col justify-end">
|
||||
{#await movieDataP then movie}
|
||||
{#await $movieDataP then movie}
|
||||
{#if movie}
|
||||
<div
|
||||
class={classNames(
|
||||
@@ -46,7 +60,7 @@
|
||||
{movie?.title}
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2"
|
||||
class="flex items-center gap-1 uppercase text-zinc-300 font-semibold tracking-wider mt-2 text-lg"
|
||||
>
|
||||
<p class="flex-shrink-0">
|
||||
{new Date(movie.release_date || Date.now())?.getFullYear()}
|
||||
@@ -65,12 +79,32 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/await}
|
||||
{#await jellyfinItem then item}
|
||||
{#if item}
|
||||
<div class="flex mt-4">
|
||||
<Button on:click={() => (playbackId = item.Id || '')}>Play</Button>
|
||||
</div>
|
||||
{/if}
|
||||
{#await Promise.all([$jellyfinItemP, $radarrItemP]) then [jellyfinItem, radarrItem]}
|
||||
<Container direction="horizontal" class="flex mt-8 gap-2">
|
||||
{#if jellyfinItem}
|
||||
<Button on:click={() => (playbackId = jellyfinItem.Id || '')}>Play</Button>
|
||||
{:else if radarrItem}
|
||||
<Button>Request</Button>
|
||||
{:else}
|
||||
<Button
|
||||
on:click={() => requests.handleAddToRadarr(Number(id))}
|
||||
inactive={$isFetching.handleAddToRadarr}
|
||||
>
|
||||
Add to Radarr
|
||||
<Plus slot="icon" size={19} />
|
||||
</Button>
|
||||
{/if}
|
||||
{#if PLATFORM_WEB}
|
||||
<Button>
|
||||
Open In TMDB
|
||||
<ExternalLink size={19} slot="icon-after" />
|
||||
</Button>
|
||||
<Button>
|
||||
Open In Jellyfin
|
||||
<ExternalLink size={19} slot="icon-after" />
|
||||
</Button>
|
||||
{/if}
|
||||
</Container>
|
||||
{/await}
|
||||
</div>
|
||||
</HeroCarousel>
|
||||
|
||||
@@ -7,7 +7,7 @@ import {
|
||||
type SonarrDownload,
|
||||
type SonarrSeries
|
||||
} from '../apis/sonarr/sonarrApi';
|
||||
import { getRadarrDownloads, getRadarrMovies, type RadarrDownload } from '../apis/radarr/radarrApi';
|
||||
import { radarrApi, type RadarrDownload } from '../apis/radarr/radarr-api';
|
||||
|
||||
async function waitForSettings() {
|
||||
return new Promise((resolve) => {
|
||||
@@ -89,7 +89,7 @@ export function createJellyfinItemStore(tmdbId: number | Promise<number>) {
|
||||
}
|
||||
|
||||
export const sonarrSeriesStore = _createDataFetchStore(getSonarrSeries);
|
||||
export const radarrMoviesStore = _createDataFetchStore(getRadarrMovies);
|
||||
export const radarrMoviesStore = _createDataFetchStore(radarrApi.getRadarrMovies);
|
||||
|
||||
export function createRadarrMovieStore(tmdbId: number) {
|
||||
const store = derived(radarrMoviesStore, (s) => {
|
||||
@@ -137,7 +137,7 @@ export function createSonarrSeriesStore(name: Promise<string> | string) {
|
||||
}
|
||||
|
||||
export const sonarrDownloadsStore = _createDataFetchStore(getSonarrDownloads);
|
||||
export const radarrDownloadsStore = _createDataFetchStore(getRadarrDownloads);
|
||||
export const radarrDownloadsStore = _createDataFetchStore(radarrApi.getRadarrDownloads);
|
||||
export const servarrDownloadsStore = (() => {
|
||||
const store = derived([sonarrDownloadsStore, radarrDownloadsStore], ([sonarr, radarr]) => {
|
||||
return {
|
||||
@@ -213,3 +213,86 @@ export function createSonarrDownloadStore(
|
||||
refresh: async () => sonarrDownloadsStore.refresh()
|
||||
};
|
||||
}
|
||||
|
||||
export const useActionRequests = <P extends Record<string, (...args: any[]) => Promise<any>>>(
|
||||
values: P
|
||||
) => {
|
||||
const initialFetching: Record<keyof P, boolean> = {} as any;
|
||||
Object.keys(values).forEach((key) => {
|
||||
initialFetching[key as keyof P] = false;
|
||||
});
|
||||
|
||||
const fetching = writable(initialFetching);
|
||||
|
||||
const initialData: Record<keyof P, Awaited<ReturnType<P[keyof P]>> | undefined> = {} as any;
|
||||
Object.keys(values).forEach((key) => {
|
||||
initialData[key as keyof P] = undefined;
|
||||
});
|
||||
|
||||
const data = writable(initialData);
|
||||
|
||||
const methods: P = {} as any;
|
||||
Object.keys(values).forEach((key) => {
|
||||
// @ts-expect-error
|
||||
methods[key as keyof P] = async (...args: any[]) => {
|
||||
fetching.update((f) => ({ ...f, [key]: true }));
|
||||
values[key as keyof P]?.(...args)
|
||||
.then((d) => data.update((prev) => ({ ...prev, [key]: d })))
|
||||
.finally(() => {
|
||||
fetching.update((f) => ({ ...f, [key]: false }));
|
||||
});
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
requests: methods,
|
||||
data: {
|
||||
subscribe: data.subscribe
|
||||
},
|
||||
isFetching: {
|
||||
subscribe: fetching.subscribe
|
||||
}
|
||||
};
|
||||
};
|
||||
export const useRequest = <P extends (...args: A) => Promise<any>, A extends any[]>(
|
||||
fn: P,
|
||||
...initialArgs: A
|
||||
) => {
|
||||
const request = writable<ReturnType<P>>(undefined);
|
||||
const data = writable<Awaited<ReturnType<P>> | undefined>(undefined);
|
||||
const isLoading = writable(true);
|
||||
const isFetching = writable(true);
|
||||
|
||||
function refresh(...args: A): ReturnType<P> {
|
||||
isFetching.set(true);
|
||||
const p: ReturnType<P> = fn(...args)
|
||||
.then((res) => {
|
||||
data.set(res);
|
||||
return res;
|
||||
})
|
||||
.finally(() => {
|
||||
isFetching.set(false);
|
||||
});
|
||||
|
||||
request.set(p);
|
||||
return p;
|
||||
}
|
||||
|
||||
refresh(...initialArgs);
|
||||
|
||||
return {
|
||||
promise: {
|
||||
subscribe: request.subscribe
|
||||
},
|
||||
data: {
|
||||
subscribe: data.subscribe
|
||||
},
|
||||
isLoading: {
|
||||
subscribe: isLoading.subscribe
|
||||
},
|
||||
isFetching: {
|
||||
subscribe: isFetching.subscribe
|
||||
},
|
||||
refresh
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
import { get, writable } from 'svelte/store';
|
||||
import type { SonarrSeries } from './apis/sonarr/sonarrApi';
|
||||
import type { RadarrMovie } from './apis/radarr/radarrApi';
|
||||
import { settings } from './stores/settings.store';
|
||||
import { writable } from 'svelte/store';
|
||||
|
||||
export function formatMinutesToTime(minutes: number) {
|
||||
const days = Math.floor(minutes / 60 / 24);
|
||||
|
||||
Reference in New Issue
Block a user