summaryrefslogtreecommitdiffhomepage
path: root/frontend/src/apis
diff options
context:
space:
mode:
authorLiang Yi <[email protected]>2022-01-22 21:35:11 +0800
committerGitHub <[email protected]>2022-01-22 21:35:11 +0800
commitd8d2300980ca69a4ae6511cb49a6dc548c0da793 (patch)
tree23f2f136c495b4064f43a0c4148391c46b9fa997 /frontend/src/apis
parent6b82a734e2bc597b219472774c0ec58038630c65 (diff)
downloadbazarr-d8d2300980ca69a4ae6511cb49a6dc548c0da793.tar.gz
bazarr-d8d2300980ca69a4ae6511cb49a6dc548c0da793.zip
Add React-Query to improve network and cache performancev1.0.3-beta.15
Diffstat (limited to 'frontend/src/apis')
-rw-r--r--frontend/src/apis/hooks.ts31
-rw-r--r--frontend/src/apis/hooks/episodes.ts115
-rw-r--r--frontend/src/apis/hooks/histories.ts21
-rw-r--r--frontend/src/apis/hooks/index.ts9
-rw-r--r--frontend/src/apis/hooks/languages.ts23
-rw-r--r--frontend/src/apis/hooks/movies.ts138
-rw-r--r--frontend/src/apis/hooks/providers.ts99
-rw-r--r--frontend/src/apis/hooks/series.ts80
-rw-r--r--frontend/src/apis/hooks/status.ts18
-rw-r--r--frontend/src/apis/hooks/subtitles.ts119
-rw-r--r--frontend/src/apis/hooks/system.ts188
-rw-r--r--frontend/src/apis/queries/client.ts (renamed from frontend/src/apis/index.ts)23
-rw-r--r--frontend/src/apis/queries/hooks.ts116
-rw-r--r--frontend/src/apis/queries/index.ts14
-rw-r--r--frontend/src/apis/queries/keys.ts23
-rw-r--r--frontend/src/apis/raw/badges.ts (renamed from frontend/src/apis/badges.ts)0
-rw-r--r--frontend/src/apis/raw/base.ts (renamed from frontend/src/apis/base.ts)10
-rw-r--r--frontend/src/apis/raw/episodes.ts (renamed from frontend/src/apis/episodes.ts)10
-rw-r--r--frontend/src/apis/raw/files.ts (renamed from frontend/src/apis/files.ts)0
-rw-r--r--frontend/src/apis/raw/history.ts (renamed from frontend/src/apis/history.ts)4
-rw-r--r--frontend/src/apis/raw/index.ts25
-rw-r--r--frontend/src/apis/raw/movies.ts (renamed from frontend/src/apis/movies.ts)26
-rw-r--r--frontend/src/apis/raw/providers.ts (renamed from frontend/src/apis/providers.ts)0
-rw-r--r--frontend/src/apis/raw/series.ts (renamed from frontend/src/apis/series.ts)9
-rw-r--r--frontend/src/apis/raw/subtitles.ts (renamed from frontend/src/apis/subtitles.ts)0
-rw-r--r--frontend/src/apis/raw/system.ts (renamed from frontend/src/apis/system.ts)2
-rw-r--r--frontend/src/apis/raw/utils.ts (renamed from frontend/src/apis/utils.ts)6
27 files changed, 1032 insertions, 77 deletions
diff --git a/frontend/src/apis/hooks.ts b/frontend/src/apis/hooks.ts
deleted file mode 100644
index 084efb63f..000000000
--- a/frontend/src/apis/hooks.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import { useCallback, useRef, useState } from "react";
-
-type Request = (...args: any[]) => Promise<any>;
-type Return<T extends Request> = PromiseType<ReturnType<T>>;
-
-export function useAsyncRequest<F extends Request>(
- request: F
-): [Async.Item<Return<F>>, (...args: Parameters<F>) => void] {
- const [state, setState] = useState<Async.Item<Return<F>>>({
- state: "uninitialized",
- content: null,
- error: null,
- });
-
- const requestRef = useRef(request);
-
- const update = useCallback(
- (...args: Parameters<F>) => {
- setState((s) => ({ ...s, state: "loading" }));
- requestRef
- .current(...args)
- .then((res) =>
- setState({ state: "succeeded", content: res, error: null })
- )
- .catch((error) => setState((s) => ({ ...s, state: "failed", error })));
- },
- [requestRef]
- );
-
- return [state, update];
-}
diff --git a/frontend/src/apis/hooks/episodes.ts b/frontend/src/apis/hooks/episodes.ts
new file mode 100644
index 000000000..d67d2d194
--- /dev/null
+++ b/frontend/src/apis/hooks/episodes.ts
@@ -0,0 +1,115 @@
+import {
+ QueryClient,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "react-query";
+import { usePaginationQuery } from "../queries/hooks";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+const cacheEpisodes = (client: QueryClient, episodes: Item.Episode[]) => {
+ episodes.forEach((item) => {
+ client.setQueryData(
+ [
+ QueryKeys.Series,
+ item.sonarrSeriesId,
+ QueryKeys.Episodes,
+ item.sonarrEpisodeId,
+ ],
+ item
+ );
+ });
+};
+
+export function useEpisodesByIds(ids: number[]) {
+ const client = useQueryClient();
+ return useQuery(
+ [QueryKeys.Series, QueryKeys.Episodes, ids],
+ () => api.episodes.byEpisodeId(ids),
+ {
+ onSuccess: (data) => {
+ cacheEpisodes(client, data);
+ },
+ }
+ );
+}
+
+export function useEpisodesBySeriesId(id: number) {
+ const client = useQueryClient();
+ return useQuery(
+ [QueryKeys.Series, id, QueryKeys.Episodes, QueryKeys.All],
+ () => api.episodes.bySeriesId([id]),
+ {
+ onSuccess: (data) => {
+ cacheEpisodes(client, data);
+ },
+ }
+ );
+}
+
+export function useEpisodeWantedPagination() {
+ return usePaginationQuery([QueryKeys.Series, QueryKeys.Wanted], (param) =>
+ api.episodes.wanted(param)
+ );
+}
+
+export function useEpisodeBlacklist() {
+ return useQuery(
+ [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
+ () => api.episodes.blacklist()
+ );
+}
+
+export function useEpisodeAddBlacklist() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
+ (param: {
+ seriesId: number;
+ episodeId: number;
+ form: FormType.AddBlacklist;
+ }) => {
+ const { seriesId, episodeId, form } = param;
+ return api.episodes.addBlacklist(seriesId, episodeId, form);
+ },
+ {
+ onSuccess: (_, { seriesId, episodeId }) => {
+ client.invalidateQueries([QueryKeys.Series, QueryKeys.Blacklist]);
+ client.invalidateQueries([QueryKeys.Series, seriesId]);
+ },
+ }
+ );
+}
+
+export function useEpisodeDeleteBlacklist() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.Blacklist],
+ (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
+ api.episodes.deleteBlacklist(param.all, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Series, QueryKeys.Blacklist]);
+ },
+ }
+ );
+}
+
+export function useEpisodeHistoryPagination() {
+ return usePaginationQuery(
+ [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History],
+ (param) => api.episodes.history(param)
+ );
+}
+
+export function useEpisodeHistory(episodeId?: number) {
+ return useQuery(
+ [QueryKeys.Series, QueryKeys.Episodes, QueryKeys.History, episodeId],
+ () => {
+ if (episodeId) {
+ return api.episodes.historyBy(episodeId);
+ }
+ }
+ );
+}
diff --git a/frontend/src/apis/hooks/histories.ts b/frontend/src/apis/hooks/histories.ts
new file mode 100644
index 000000000..53a7340ba
--- /dev/null
+++ b/frontend/src/apis/hooks/histories.ts
@@ -0,0 +1,21 @@
+import { useQuery } from "react-query";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+export function useHistoryStats(
+ time: History.TimeFrameOptions,
+ action: History.ActionOptions | null,
+ provider: System.Provider | null,
+ language: Language.Info | null
+) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.History, { time, action, provider, language }],
+ () =>
+ api.history.stats(
+ time,
+ action ?? undefined,
+ provider?.name,
+ language?.code2
+ )
+ );
+}
diff --git a/frontend/src/apis/hooks/index.ts b/frontend/src/apis/hooks/index.ts
new file mode 100644
index 000000000..34b794592
--- /dev/null
+++ b/frontend/src/apis/hooks/index.ts
@@ -0,0 +1,9 @@
+export * from "./episodes";
+export * from "./histories";
+export * from "./languages";
+export * from "./movies";
+export * from "./providers";
+export * from "./series";
+export * from "./status";
+export * from "./subtitles";
+export * from "./system";
diff --git a/frontend/src/apis/hooks/languages.ts b/frontend/src/apis/hooks/languages.ts
new file mode 100644
index 000000000..d26c46f87
--- /dev/null
+++ b/frontend/src/apis/hooks/languages.ts
@@ -0,0 +1,23 @@
+import { useQuery } from "react-query";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+export function useLanguages(history?: boolean) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Languages, history ?? false],
+ () => api.system.languages(history),
+ {
+ staleTime: Infinity,
+ }
+ );
+}
+
+export function useLanguageProfiles() {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.LanguagesProfiles],
+ () => api.system.languagesProfileList(),
+ {
+ staleTime: Infinity,
+ }
+ );
+}
diff --git a/frontend/src/apis/hooks/movies.ts b/frontend/src/apis/hooks/movies.ts
new file mode 100644
index 000000000..541e00217
--- /dev/null
+++ b/frontend/src/apis/hooks/movies.ts
@@ -0,0 +1,138 @@
+import {
+ QueryClient,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "react-query";
+import { usePaginationQuery } from "../queries/hooks";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+const cacheMovies = (client: QueryClient, movies: Item.Movie[]) => {
+ movies.forEach((item) => {
+ client.setQueryData([QueryKeys.Movies, item.radarrId], item);
+ });
+};
+
+export function useMoviesByIds(ids: number[]) {
+ const client = useQueryClient();
+ return useQuery([QueryKeys.Movies, ...ids], () => api.movies.movies(ids), {
+ onSuccess: (data) => {
+ cacheMovies(client, data);
+ },
+ });
+}
+
+export function useMovieById(id: number) {
+ return useQuery([QueryKeys.Movies, id], async () => {
+ const response = await api.movies.movies([id]);
+ return response.length > 0 ? response[0] : undefined;
+ });
+}
+
+export function useMovies() {
+ const client = useQueryClient();
+ return useQuery(
+ [QueryKeys.Movies, QueryKeys.All],
+ () => api.movies.movies(),
+ {
+ enabled: false,
+ onSuccess: (data) => {
+ cacheMovies(client, data);
+ },
+ }
+ );
+}
+
+export function useMoviesPagination() {
+ return usePaginationQuery([QueryKeys.Movies], (param) =>
+ api.movies.moviesBy(param)
+ );
+}
+
+export function useMovieModification() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Movies],
+ (form: FormType.ModifyItem) => api.movies.modify(form),
+ {
+ onSuccess: (_, form) => {
+ form.id.forEach((v) => {
+ client.invalidateQueries([QueryKeys.Movies, v]);
+ });
+ // TODO: query less
+ client.invalidateQueries([QueryKeys.Movies]);
+ },
+ }
+ );
+}
+
+export function useMovieAction() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Actions, QueryKeys.Movies],
+ (form: FormType.MoviesAction) => api.movies.action(form),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.Movies]);
+ },
+ }
+ );
+}
+
+export function useMovieWantedPagination() {
+ return usePaginationQuery([QueryKeys.Movies, QueryKeys.Wanted], (param) =>
+ api.movies.wanted(param)
+ );
+}
+
+export function useMovieBlacklist() {
+ return useQuery([QueryKeys.Movies, QueryKeys.Blacklist], () =>
+ api.movies.blacklist()
+ );
+}
+
+export function useMovieAddBlacklist() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Movies, QueryKeys.Blacklist],
+ (param: { id: number; form: FormType.AddBlacklist }) => {
+ const { id, form } = param;
+ return api.movies.addBlacklist(id, form);
+ },
+ {
+ onSuccess: (_, { id }) => {
+ client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
+ client.invalidateQueries([QueryKeys.Movies, id]);
+ },
+ }
+ );
+}
+
+export function useMovieDeleteBlacklist() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Movies, QueryKeys.Blacklist],
+ (param: { all?: boolean; form?: FormType.DeleteBlacklist }) =>
+ api.movies.deleteBlacklist(param.all, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Movies, QueryKeys.Blacklist]);
+ },
+ }
+ );
+}
+
+export function useMovieHistoryPagination() {
+ return usePaginationQuery([QueryKeys.Movies, QueryKeys.History], (param) =>
+ api.movies.history(param)
+ );
+}
+
+export function useMovieHistory(radarrId?: number) {
+ return useQuery([QueryKeys.Movies, QueryKeys.History, radarrId], () => {
+ if (radarrId) {
+ return api.movies.historyBy(radarrId);
+ }
+ });
+}
diff --git a/frontend/src/apis/hooks/providers.ts b/frontend/src/apis/hooks/providers.ts
new file mode 100644
index 000000000..f1daf9f37
--- /dev/null
+++ b/frontend/src/apis/hooks/providers.ts
@@ -0,0 +1,99 @@
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+export function useSystemProviders(history?: boolean) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Providers, history ?? false],
+ () => api.providers.providers(history)
+ );
+}
+
+export function useMoviesProvider(radarrId?: number) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Providers, QueryKeys.Movies, radarrId],
+ () => {
+ if (radarrId) {
+ return api.providers.movies(radarrId);
+ }
+ },
+ {
+ staleTime: Infinity,
+ }
+ );
+}
+
+export function useEpisodesProvider(episodeId?: number) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Providers, QueryKeys.Episodes, episodeId],
+ () => {
+ if (episodeId) {
+ return api.providers.episodes(episodeId);
+ }
+ },
+ {
+ staleTime: Infinity,
+ }
+ );
+}
+
+export function useResetProvider() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.System, QueryKeys.Providers],
+ () => api.providers.reset(),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.System, QueryKeys.Providers]);
+ },
+ }
+ );
+}
+
+export function useDownloadEpisodeSubtitles() {
+ const client = useQueryClient();
+
+ return useMutation(
+ [
+ QueryKeys.System,
+ QueryKeys.Providers,
+ QueryKeys.Subtitles,
+ QueryKeys.Episodes,
+ ],
+ (param: {
+ seriesId: number;
+ episodeId: number;
+ form: FormType.ManualDownload;
+ }) =>
+ api.providers.downloadEpisodeSubtitle(
+ param.seriesId,
+ param.episodeId,
+ param.form
+ ),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Series, param.seriesId]);
+ },
+ }
+ );
+}
+
+export function useDownloadMovieSubtitles() {
+ const client = useQueryClient();
+
+ return useMutation(
+ [
+ QueryKeys.System,
+ QueryKeys.Providers,
+ QueryKeys.Subtitles,
+ QueryKeys.Movies,
+ ],
+ (param: { radarrId: number; form: FormType.ManualDownload }) =>
+ api.providers.downloadMovieSubtitle(param.radarrId, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
+ },
+ }
+ );
+}
diff --git a/frontend/src/apis/hooks/series.ts b/frontend/src/apis/hooks/series.ts
new file mode 100644
index 000000000..4de9f5c1b
--- /dev/null
+++ b/frontend/src/apis/hooks/series.ts
@@ -0,0 +1,80 @@
+import {
+ QueryClient,
+ useMutation,
+ useQuery,
+ useQueryClient,
+} from "react-query";
+import { usePaginationQuery } from "../queries/hooks";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+function cacheSeries(client: QueryClient, series: Item.Series[]) {
+ series.forEach((item) => {
+ client.setQueryData([QueryKeys.Series, item.sonarrSeriesId], item);
+ });
+}
+
+export function useSeriesByIds(ids: number[]) {
+ const client = useQueryClient();
+ return useQuery([QueryKeys.Series, ...ids], () => api.series.series(ids), {
+ onSuccess: (data) => {
+ cacheSeries(client, data);
+ },
+ });
+}
+
+export function useSeriesById(id: number) {
+ return useQuery([QueryKeys.Series, id], async () => {
+ const response = await api.series.series([id]);
+ return response.length > 0 ? response[0] : undefined;
+ });
+}
+
+export function useSeries() {
+ const client = useQueryClient();
+ return useQuery(
+ [QueryKeys.Series, QueryKeys.All],
+ () => api.series.series(),
+ {
+ enabled: false,
+ onSuccess: (data) => {
+ cacheSeries(client, data);
+ },
+ }
+ );
+}
+
+export function useSeriesPagination() {
+ return usePaginationQuery([QueryKeys.Series], (param) =>
+ api.series.seriesBy(param)
+ );
+}
+
+export function useSeriesModification() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Series],
+ (form: FormType.ModifyItem) => api.series.modify(form),
+ {
+ onSuccess: (_, form) => {
+ form.id.forEach((v) => {
+ client.invalidateQueries([QueryKeys.Series, v]);
+ });
+ client.invalidateQueries([QueryKeys.Series]);
+ },
+ }
+ );
+}
+
+export function useSeriesAction() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.Actions, QueryKeys.Series],
+ (form: FormType.SeriesAction) => api.series.action(form),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.Series]);
+ },
+ }
+ );
+}
diff --git a/frontend/src/apis/hooks/status.ts b/frontend/src/apis/hooks/status.ts
new file mode 100644
index 000000000..46a73cfda
--- /dev/null
+++ b/frontend/src/apis/hooks/status.ts
@@ -0,0 +1,18 @@
+import { useIsMutating } from "react-query";
+import { QueryKeys } from "../queries/keys";
+
+export function useIsAnyActionRunning() {
+ return useIsMutating([QueryKeys.Actions]) > 0;
+}
+
+export function useIsMovieActionRunning() {
+ return useIsMutating([QueryKeys.Actions, QueryKeys.Movies]) > 0;
+}
+
+export function useIsSeriesActionRunning() {
+ return useIsMutating([QueryKeys.Actions, QueryKeys.Series]) > 0;
+}
+
+export function useIsAnyMutationRunning() {
+ return useIsMutating() > 0;
+}
diff --git a/frontend/src/apis/hooks/subtitles.ts b/frontend/src/apis/hooks/subtitles.ts
new file mode 100644
index 000000000..5080daeb7
--- /dev/null
+++ b/frontend/src/apis/hooks/subtitles.ts
@@ -0,0 +1,119 @@
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+export function useSubtitleAction() {
+ const client = useQueryClient();
+ interface Param {
+ action: string;
+ form: FormType.ModifySubtitle;
+ }
+ return useMutation(
+ [QueryKeys.Subtitles],
+ (param: Param) => api.subtitles.modify(param.action, param.form),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.History]);
+ },
+ }
+ );
+}
+
+export function useEpisodeSubtitleModification() {
+ const client = useQueryClient();
+
+ interface Param<T> {
+ seriesId: number;
+ episodeId: number;
+ form: T;
+ }
+
+ const download = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Episodes],
+ (param: Param<FormType.Subtitle>) =>
+ api.episodes.downloadSubtitles(
+ param.seriesId,
+ param.episodeId,
+ param.form
+ ),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Series, param.seriesId]);
+ },
+ }
+ );
+
+ const remove = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Episodes],
+ (param: Param<FormType.DeleteSubtitle>) =>
+ api.episodes.deleteSubtitles(param.seriesId, param.episodeId, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Series, param.seriesId]);
+ },
+ }
+ );
+
+ const upload = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Episodes],
+ (param: Param<FormType.UploadSubtitle>) =>
+ api.episodes.uploadSubtitles(param.seriesId, param.episodeId, param.form),
+ {
+ onSuccess: (_, { seriesId }) => {
+ client.invalidateQueries([QueryKeys.Series, seriesId]);
+ },
+ }
+ );
+
+ return { download, remove, upload };
+}
+
+export function useMovieSubtitleModification() {
+ const client = useQueryClient();
+
+ interface Param<T> {
+ radarrId: number;
+ form: T;
+ }
+
+ const download = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Movies],
+ (param: Param<FormType.Subtitle>) =>
+ api.movies.downloadSubtitles(param.radarrId, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
+ },
+ }
+ );
+
+ const remove = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Movies],
+ (param: Param<FormType.DeleteSubtitle>) =>
+ api.movies.deleteSubtitles(param.radarrId, param.form),
+ {
+ onSuccess: (_, param) => {
+ client.invalidateQueries([QueryKeys.Movies, param.radarrId]);
+ },
+ }
+ );
+
+ const upload = useMutation(
+ [QueryKeys.Subtitles, QueryKeys.Movies],
+ (param: Param<FormType.UploadSubtitle>) =>
+ api.movies.uploadSubtitles(param.radarrId, param.form),
+ {
+ onSuccess: (_, { radarrId }) => {
+ client.invalidateQueries([QueryKeys.Movies, radarrId]);
+ },
+ }
+ );
+
+ return { download, remove, upload };
+}
+
+export function useSubtitleInfos(names: string[]) {
+ return useQuery([QueryKeys.Subtitles, QueryKeys.Infos, names], () =>
+ api.subtitles.info(names)
+ );
+}
diff --git a/frontend/src/apis/hooks/system.ts b/frontend/src/apis/hooks/system.ts
new file mode 100644
index 000000000..f096806b8
--- /dev/null
+++ b/frontend/src/apis/hooks/system.ts
@@ -0,0 +1,188 @@
+import { useMemo } from "react";
+import { useMutation, useQuery, useQueryClient } from "react-query";
+import { setUnauthenticated } from "../../@redux/actions";
+import store from "../../@redux/store";
+import { QueryKeys } from "../queries/keys";
+import api from "../raw";
+
+export function useBadges() {
+ return useQuery([QueryKeys.System, QueryKeys.Badges], () => api.badges.all());
+}
+
+export function useFileSystem(
+ type: "bazarr" | "sonarr" | "radarr",
+ path: string,
+ enabled: boolean
+) {
+ return useQuery(
+ [QueryKeys.FileSystem, type, path],
+ () => {
+ if (type === "bazarr") {
+ return api.files.bazarr(path);
+ } else if (type === "radarr") {
+ return api.files.radarr(path);
+ } else if (type === "sonarr") {
+ return api.files.sonarr(path);
+ }
+ },
+ {
+ enabled,
+ }
+ );
+}
+
+export function useSystemSettings() {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Settings],
+ () => api.system.settings(),
+ {
+ staleTime: Infinity,
+ }
+ );
+}
+
+export function useSettingsMutation() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.System, QueryKeys.Settings],
+ (data: LooseObject) => api.system.updateSettings(data),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.System]);
+ },
+ }
+ );
+}
+
+export function useServerSearch(query: string, enabled: boolean) {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Search, query],
+ () => api.system.search(query),
+ {
+ enabled,
+ }
+ );
+}
+
+export function useSystemLogs() {
+ return useQuery([QueryKeys.System, QueryKeys.Logs], () => api.system.logs(), {
+ refetchOnWindowFocus: "always",
+ refetchInterval: 1000 * 60,
+ staleTime: 1000,
+ });
+}
+
+export function useDeleteLogs() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.System, QueryKeys.Logs],
+ () => api.system.deleteLogs(),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.System, QueryKeys.Logs]);
+ },
+ }
+ );
+}
+
+export function useSystemTasks() {
+ return useQuery(
+ [QueryKeys.System, QueryKeys.Tasks],
+ () => api.system.tasks(),
+ {
+ refetchOnWindowFocus: "always",
+ refetchInterval: 1000 * 60,
+ staleTime: 1000 * 10,
+ }
+ );
+}
+
+export function useRunTask() {
+ const client = useQueryClient();
+ return useMutation(
+ [QueryKeys.System, QueryKeys.Tasks],
+ (id: string) => api.system.runTask(id),
+ {
+ onSuccess: () => {
+ client.invalidateQueries([QueryKeys.System, QueryKeys.Tasks]);
+ },
+ }
+ );
+}
+
+export function useSystemStatus() {
+ return useQuery([QueryKeys.System, "status"], () => api.system.status());
+}
+
+export function useSystemHealth() {
+ return useQuery([QueryKeys.System, "health"], () => api.system.health());
+}
+
+export function useSystemReleases() {
+ return useQuery([QueryKeys.System, "releases"], () => api.system.releases());
+}
+
+export function useSystem() {
+ const client = useQueryClient();
+ const { mutate: logout, isLoading: isLoggingOut } = useMutation(
+ [QueryKeys.System, QueryKeys.Actions],
+ () => api.system.logout(),
+ {
+ onSuccess: () => {
+ store.dispatch(setUnauthenticated());
+ client.clear();
+ },
+ }
+ );
+
+ const { mutate: login, isLoading: isLoggingIn } = useMutation(
+ [QueryKeys.System, QueryKeys.Actions],
+ (param: { username: string; password: string }) =>
+ api.system.login(param.username, param.password),
+ {
+ onSuccess: () => {
+ window.location.reload();
+ },
+ }
+ );
+
+ const { mutate: shutdown, isLoading: isShuttingDown } = useMutation(
+ [QueryKeys.System, QueryKeys.Actions],
+ () => api.system.shutdown(),
+ {
+ onSuccess: () => {
+ client.clear();
+ },
+ }
+ );
+
+ const { mutate: restart, isLoading: isRestarting } = useMutation(
+ [QueryKeys.System, QueryKeys.Actions],
+ () => api.system.restart(),
+ {
+ onSuccess: () => {
+ client.clear();
+ },
+ }
+ );
+
+ return useMemo(
+ () => ({
+ logout,
+ shutdown,
+ restart,
+ login,
+ isWorking: isLoggingOut || isShuttingDown || isRestarting || isLoggingIn,
+ }),
+ [
+ isLoggingIn,
+ isLoggingOut,
+ isRestarting,
+ isShuttingDown,
+ login,
+ logout,
+ restart,
+ shutdown,
+ ]
+ );
+}
diff --git a/frontend/src/apis/index.ts b/frontend/src/apis/queries/client.ts
index 3efc0aba1..2263874bc 100644
--- a/frontend/src/apis/index.ts
+++ b/frontend/src/apis/queries/client.ts
@@ -1,8 +1,8 @@
import Axios, { AxiosError, AxiosInstance, CancelTokenSource } from "axios";
-import { siteRedirectToAuth } from "../@redux/actions";
-import { AppDispatch } from "../@redux/store";
-import { Environment, isProdEnv } from "../utilities";
-class Api {
+import { setUnauthenticated } from "../../@redux/actions";
+import { AppDispatch } from "../../@redux/store";
+import { Environment, isProdEnv } from "../../utilities";
+class BazarrClient {
axios!: AxiosInstance;
source!: CancelTokenSource;
dispatch!: AppDispatch;
@@ -57,7 +57,7 @@ class Api {
handleError(code: number) {
switch (code) {
case 401:
- this.dispatch(siteRedirectToAuth());
+ this.dispatch(setUnauthenticated());
break;
case 500:
break;
@@ -67,15 +67,4 @@ class Api {
}
}
-export default new Api();
-export { default as BadgesApi } from "./badges";
-export { default as EpisodesApi } from "./episodes";
-export { default as FilesApi } from "./files";
-export { default as HistoryApi } from "./history";
-export * from "./hooks";
-export { default as MoviesApi } from "./movies";
-export { default as ProvidersApi } from "./providers";
-export { default as SeriesApi } from "./series";
-export { default as SubtitlesApi } from "./subtitles";
-export { default as SystemApi } from "./system";
-export { default as UtilsApi } from "./utils";
+export default new BazarrClient();
diff --git a/frontend/src/apis/queries/hooks.ts b/frontend/src/apis/queries/hooks.ts
new file mode 100644
index 000000000..b8cc52c9c
--- /dev/null
+++ b/frontend/src/apis/queries/hooks.ts
@@ -0,0 +1,116 @@
+import { useCallback, useEffect, useState } from "react";
+import {
+ QueryKey,
+ useQuery,
+ useQueryClient,
+ UseQueryResult,
+} from "react-query";
+import { GetItemId } from "utilities";
+import { usePageSize } from "utilities/storage";
+import { QueryKeys } from "./keys";
+
+export type UsePaginationQueryResult<T extends object> = UseQueryResult<
+ DataWrapperWithTotal<T>
+> & {
+ controls: {
+ previousPage: () => void;
+ nextPage: () => void;
+ gotoPage: (index: number) => void;
+ };
+ paginationStatus: {
+ totalCount: number;
+ pageSize: number;
+ pageCount: number;
+ page: number;
+ canPrevious: boolean;
+ canNext: boolean;
+ };
+};
+
+export function usePaginationQuery<
+ TObject extends object = object,
+ TQueryKey extends QueryKey = QueryKey
+>(
+ queryKey: TQueryKey,
+ queryFn: RangeQuery<TObject>
+): UsePaginationQueryResult<TObject> {
+ const client = useQueryClient();
+
+ const [page, setIndex] = useState(0);
+ const [pageSize] = usePageSize();
+
+ const start = page * pageSize;
+
+ const results = useQuery(
+ [...queryKey, QueryKeys.Range, { start, size: pageSize }],
+ () => {
+ const param: Parameter.Range = {
+ start,
+ length: pageSize,
+ };
+ return queryFn(param);
+ },
+ {
+ onSuccess: ({ data }) => {
+ data.forEach((item) => {
+ const id = GetItemId(item);
+ if (id) {
+ client.setQueryData([...queryKey, id], item);
+ }
+ });
+ },
+ }
+ );
+
+ const { data } = results;
+
+ const totalCount = data?.total ?? 0;
+ const pageCount = Math.ceil(totalCount / pageSize);
+
+ const previousPage = useCallback(() => {
+ setIndex((index) => Math.max(0, index - 1));
+ }, []);
+
+ const nextPage = useCallback(() => {
+ if (pageCount > 0) {
+ setIndex((index) => Math.min(pageCount - 1, index + 1));
+ }
+ }, [pageCount]);
+
+ const gotoPage = useCallback(
+ (idx: number) => {
+ if (idx >= 0 && idx < pageCount) {
+ setIndex(idx);
+ }
+ },
+ [pageCount]
+ );
+
+ // Reset page index if we out of bound
+ useEffect(() => {
+ if (pageCount === 0) return;
+
+ if (page >= pageCount) {
+ setIndex(pageCount - 1);
+ } else if (page < 0) {
+ setIndex(0);
+ }
+ }, [page, pageCount]);
+
+ return {
+ ...results,
+ paginationStatus: {
+ totalCount,
+ pageCount,
+ pageSize,
+ page,
+ canPrevious: page > 0,
+ canNext: page < pageCount - 1,
+ },
+ controls: {
+ gotoPage,
+ previousPage,
+ nextPage,
+ },
+ };
+}
diff --git a/frontend/src/apis/queries/index.ts b/frontend/src/apis/queries/index.ts
new file mode 100644
index 000000000..a1a17ffd9
--- /dev/null
+++ b/frontend/src/apis/queries/index.ts
@@ -0,0 +1,14 @@
+import { QueryClient } from "react-query";
+
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ refetchOnWindowFocus: false,
+ retry: false,
+ staleTime: 1000 * 60,
+ keepPreviousData: true,
+ },
+ },
+});
+
+export default queryClient;
diff --git a/frontend/src/apis/queries/keys.ts b/frontend/src/apis/queries/keys.ts
new file mode 100644
index 000000000..cfdd44133
--- /dev/null
+++ b/frontend/src/apis/queries/keys.ts
@@ -0,0 +1,23 @@
+export enum QueryKeys {
+ Movies = "movies",
+ Episodes = "episodes",
+ Series = "series",
+ Badges = "badges",
+ FileSystem = "file-system",
+ System = "system",
+ Settings = "settings",
+ Subtitles = "subtitles",
+ Providers = "providers",
+ Languages = "languages",
+ LanguagesProfiles = "languages-profiles",
+ Blacklist = "blacklist",
+ Search = "search",
+ Actions = "actions",
+ Tasks = "tasks",
+ Logs = "logs",
+ Infos = "infos",
+ History = "history",
+ Wanted = "wanted",
+ Range = "range",
+ All = "all",
+}
diff --git a/frontend/src/apis/badges.ts b/frontend/src/apis/raw/badges.ts
index 0021dede6..0021dede6 100644
--- a/frontend/src/apis/badges.ts
+++ b/frontend/src/apis/raw/badges.ts
diff --git a/frontend/src/apis/base.ts b/frontend/src/apis/raw/base.ts
index 3a0ab4bb9..2c514adf9 100644
--- a/frontend/src/apis/base.ts
+++ b/frontend/src/apis/raw/base.ts
@@ -1,5 +1,5 @@
import { AxiosResponse } from "axios";
-import apis from ".";
+import client from "../queries/client";
class BaseApi {
prefix: string;
@@ -31,7 +31,7 @@ class BaseApi {
}
protected async get<T = unknown>(path: string, params?: any) {
- const response = await apis.axios.get<T>(this.prefix + path, { params });
+ const response = await client.axios.get<T>(this.prefix + path, { params });
return response.data;
}
@@ -41,7 +41,7 @@ class BaseApi {
params?: any
): Promise<AxiosResponse<T>> {
const form = this.createFormdata(formdata);
- return apis.axios.post(this.prefix + path, form, { params });
+ return client.axios.post(this.prefix + path, form, { params });
}
protected patch<T = void>(
@@ -50,7 +50,7 @@ class BaseApi {
params?: any
): Promise<AxiosResponse<T>> {
const form = this.createFormdata(formdata);
- return apis.axios.patch(this.prefix + path, form, { params });
+ return client.axios.patch(this.prefix + path, form, { params });
}
protected delete<T = void>(
@@ -59,7 +59,7 @@ class BaseApi {
params?: any
): Promise<AxiosResponse<T>> {
const form = this.createFormdata(formdata);
- return apis.axios.delete(this.prefix + path, { params, data: form });
+ return client.axios.delete(this.prefix + path, { params, data: form });
}
}
diff --git a/frontend/src/apis/episodes.ts b/frontend/src/apis/raw/episodes.ts
index 345954b32..2075fb8e6 100644
--- a/frontend/src/apis/episodes.ts
+++ b/frontend/src/apis/raw/episodes.ts
@@ -20,7 +20,7 @@ class EpisodeApi extends BaseApi {
}
async wanted(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<Wanted.Episode>>(
+ const response = await this.get<DataWrapperWithTotal<Wanted.Episode>>(
"/wanted",
params
);
@@ -28,7 +28,7 @@ class EpisodeApi extends BaseApi {
}
async wantedBy(episodeid: number[]) {
- const response = await this.get<AsyncDataWrapper<Wanted.Episode>>(
+ const response = await this.get<DataWrapperWithTotal<Wanted.Episode>>(
"/wanted",
{ episodeid }
);
@@ -36,7 +36,7 @@ class EpisodeApi extends BaseApi {
}
async history(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<History.Episode>>(
+ const response = await this.get<DataWrapperWithTotal<History.Episode>>(
"/history",
params
);
@@ -44,11 +44,11 @@ class EpisodeApi extends BaseApi {
}
async historyBy(episodeid: number) {
- const response = await this.get<AsyncDataWrapper<History.Episode>>(
+ const response = await this.get<DataWrapperWithTotal<History.Episode>>(
"/history",
{ episodeid }
);
- return response;
+ return response.data;
}
async downloadSubtitles(
diff --git a/frontend/src/apis/files.ts b/frontend/src/apis/raw/files.ts
index 88913ac08..88913ac08 100644
--- a/frontend/src/apis/files.ts
+++ b/frontend/src/apis/raw/files.ts
diff --git a/frontend/src/apis/history.ts b/frontend/src/apis/raw/history.ts
index d26d89d8b..c1226cd7f 100644
--- a/frontend/src/apis/history.ts
+++ b/frontend/src/apis/raw/history.ts
@@ -6,13 +6,13 @@ class HistoryApi extends BaseApi {
}
async stats(
- timeframe?: History.TimeframeOptions,
+ timeFrame?: History.TimeFrameOptions,
action?: History.ActionOptions,
provider?: string,
language?: Language.CodeType
) {
const response = await this.get<History.Stat>("/stats", {
- timeframe,
+ timeFrame,
action,
provider,
language,
diff --git a/frontend/src/apis/raw/index.ts b/frontend/src/apis/raw/index.ts
new file mode 100644
index 000000000..5283f0f2c
--- /dev/null
+++ b/frontend/src/apis/raw/index.ts
@@ -0,0 +1,25 @@
+import badges from "./badges";
+import episodes from "./episodes";
+import files from "./files";
+import history from "./history";
+import movies from "./movies";
+import providers from "./providers";
+import series from "./series";
+import subtitles from "./subtitles";
+import system from "./system";
+import utils from "./utils";
+
+const api = {
+ badges,
+ episodes,
+ files,
+ movies,
+ series,
+ providers,
+ history,
+ subtitles,
+ system,
+ utils,
+};
+
+export default api;
diff --git a/frontend/src/apis/movies.ts b/frontend/src/apis/raw/movies.ts
index 8e94712cb..b8690fdcc 100644
--- a/frontend/src/apis/movies.ts
+++ b/frontend/src/apis/raw/movies.ts
@@ -21,14 +21,17 @@ class MovieApi extends BaseApi {
}
async movies(radarrid?: number[]) {
- const response = await this.get<AsyncDataWrapper<Item.Movie>>("", {
+ const response = await this.get<DataWrapperWithTotal<Item.Movie>>("", {
radarrid,
});
- return response;
+ return response.data;
}
async moviesBy(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<Item.Movie>>("", params);
+ const response = await this.get<DataWrapperWithTotal<Item.Movie>>(
+ "",
+ params
+ );
return response;
}
@@ -37,7 +40,7 @@ class MovieApi extends BaseApi {
}
async wanted(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<Wanted.Movie>>(
+ const response = await this.get<DataWrapperWithTotal<Wanted.Movie>>(
"/wanted",
params
);
@@ -45,14 +48,17 @@ class MovieApi extends BaseApi {
}
async wantedBy(radarrid: number[]) {
- const response = await this.get<AsyncDataWrapper<Wanted.Movie>>("/wanted", {
- radarrid,
- });
+ const response = await this.get<DataWrapperWithTotal<Wanted.Movie>>(
+ "/wanted",
+ {
+ radarrid,
+ }
+ );
return response;
}
async history(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<History.Movie>>(
+ const response = await this.get<DataWrapperWithTotal<History.Movie>>(
"/history",
params
);
@@ -60,11 +66,11 @@ class MovieApi extends BaseApi {
}
async historyBy(radarrid: number) {
- const response = await this.get<AsyncDataWrapper<History.Movie>>(
+ const response = await this.get<DataWrapperWithTotal<History.Movie>>(
"/history",
{ radarrid }
);
- return response;
+ return response.data;
}
async action(action: FormType.MoviesAction) {
diff --git a/frontend/src/apis/providers.ts b/frontend/src/apis/raw/providers.ts
index cfbb2dbc5..cfbb2dbc5 100644
--- a/frontend/src/apis/providers.ts
+++ b/frontend/src/apis/raw/providers.ts
diff --git a/frontend/src/apis/series.ts b/frontend/src/apis/raw/series.ts
index 976104003..d94b108df 100644
--- a/frontend/src/apis/series.ts
+++ b/frontend/src/apis/raw/series.ts
@@ -6,14 +6,17 @@ class SeriesApi extends BaseApi {
}
async series(seriesid?: number[]) {
- const response = await this.get<AsyncDataWrapper<Item.Series>>("", {
+ const response = await this.get<DataWrapperWithTotal<Item.Series>>("", {
seriesid,
});
- return response;
+ return response.data;
}
async seriesBy(params: Parameter.Range) {
- const response = await this.get<AsyncDataWrapper<Item.Series>>("", params);
+ const response = await this.get<DataWrapperWithTotal<Item.Series>>(
+ "",
+ params
+ );
return response;
}
diff --git a/frontend/src/apis/subtitles.ts b/frontend/src/apis/raw/subtitles.ts
index abfe96ba6..abfe96ba6 100644
--- a/frontend/src/apis/subtitles.ts
+++ b/frontend/src/apis/raw/subtitles.ts
diff --git a/frontend/src/apis/system.ts b/frontend/src/apis/raw/system.ts
index d21200c28..c473f076d 100644
--- a/frontend/src/apis/system.ts
+++ b/frontend/src/apis/raw/system.ts
@@ -30,7 +30,7 @@ class SystemApi extends BaseApi {
return response;
}
- async setSettings(data: object) {
+ async updateSettings(data: object) {
await this.post("/settings", data);
}
diff --git a/frontend/src/apis/utils.ts b/frontend/src/apis/raw/utils.ts
index d2adddb26..d553c8f7f 100644
--- a/frontend/src/apis/utils.ts
+++ b/frontend/src/apis/raw/utils.ts
@@ -1,4 +1,4 @@
-import apis from ".";
+import client from "../queries/client";
type UrlTestResponse =
| {
@@ -13,7 +13,7 @@ type UrlTestResponse =
class RequestUtils {
async urlTest(protocol: string, url: string, params?: any) {
try {
- const result = await apis.axios.get<UrlTestResponse>(
+ const result = await client.axios.get<UrlTestResponse>(
`../test/${protocol}/${url}api/system/status`,
{ params }
);
@@ -24,7 +24,7 @@ class RequestUtils {
throw new Error("Cannot get response, fallback to v3 api");
}
} catch (e) {
- const result = await apis.axios.get<UrlTestResponse>(
+ const result = await client.axios.get<UrlTestResponse>(
`../test/${protocol}/${url}api/v3/system/status`,
{ params }
);