feat: Request utils

This commit is contained in:
Aleksi Lassila
2024-03-31 00:25:55 +02:00
parent 26d4ba0f8f
commit 5b18c95766
13 changed files with 430 additions and 261 deletions

View File

@@ -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',
}
};

View File

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

View File

@@ -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() {

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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