mirror of
https://github.com/zoriya/Kyoo.git
synced 2025-06-03 21:54:49 -04:00
252 lines
7.1 KiB
TypeScript
252 lines
7.1 KiB
TypeScript
/*
|
|
* Kyoo - A portable and vast media library solution.
|
|
* Copyright (c) Kyoo.
|
|
*
|
|
* See AUTHORS.md and LICENSE file in the project root for full license information.
|
|
*
|
|
* Kyoo is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* any later version.
|
|
*
|
|
* Kyoo is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with Kyoo. If not, see <https://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
import { ComponentType, ReactElement } from "react";
|
|
import {
|
|
dehydrate,
|
|
QueryClient,
|
|
QueryFunctionContext,
|
|
QueryKey,
|
|
useInfiniteQuery,
|
|
useQuery,
|
|
} from "@tanstack/react-query";
|
|
import { z } from "zod";
|
|
import { KyooErrors } from "./kyoo-errors";
|
|
import { Page, Paged } from "./page";
|
|
import { Platform } from "react-native";
|
|
import { getToken } from "./login";
|
|
|
|
const kyooUrl =
|
|
Platform.OS !== "web"
|
|
? process.env.PUBLIC_BACK_URL
|
|
: typeof window === "undefined"
|
|
? process.env.KYOO_URL ?? "http://localhost:5000"
|
|
: "/api";
|
|
|
|
export let kyooApiUrl: string | null = kyooUrl || null;
|
|
|
|
export const setApiUrl = (apiUrl: string) => {
|
|
kyooApiUrl = apiUrl;
|
|
};
|
|
|
|
export const queryFn = async <Data,>(
|
|
context:
|
|
| QueryFunctionContext
|
|
| {
|
|
path: (string | false | undefined | null)[];
|
|
body?: object;
|
|
method: "GET" | "POST" | "DELETE";
|
|
authenticated?: boolean;
|
|
apiUrl?: string;
|
|
abortSignal?: AbortSignal;
|
|
},
|
|
type?: z.ZodType<Data>,
|
|
token?: string | null,
|
|
): Promise<Data> => {
|
|
// @ts-ignore
|
|
let url: string | null = context.apiUrl ?? kyooApiUrl;
|
|
if (!url) console.error("Kyoo's url is not defined.");
|
|
kyooApiUrl = url;
|
|
|
|
// @ts-ignore
|
|
if (!token && context.authenticated !== false) token = await getToken();
|
|
const path = [url]
|
|
.concat(
|
|
"path" in context
|
|
? (context.path.filter((x) => x) as string[])
|
|
: "pageParam" in context
|
|
? [context.pageParam as string]
|
|
: (context.queryKey.filter((x, i) => x && i) as string[]),
|
|
)
|
|
.join("/")
|
|
.replace("/?", "?");
|
|
let resp;
|
|
try {
|
|
resp = await fetch(path, {
|
|
// @ts-ignore
|
|
method: context.method,
|
|
// @ts-ignore
|
|
body: context.body ? JSON.stringify(context.body) : undefined,
|
|
headers: {
|
|
...(token ? { Authorization: token } : {}),
|
|
...("body" in context ? { "Content-Type": "application/json" } : {}),
|
|
},
|
|
signal: "abortSignal" in context ? context.abortSignal : undefined,
|
|
});
|
|
} catch (e) {
|
|
console.log("Fetch error", e);
|
|
throw { errors: ["Could not reach Kyoo's server."] } as KyooErrors;
|
|
}
|
|
if (resp.status === 404) {
|
|
throw { errors: ["Resource not found."] } as KyooErrors;
|
|
}
|
|
if (!resp.ok) {
|
|
const error = await resp.text();
|
|
let data;
|
|
try {
|
|
data = JSON.parse(error);
|
|
} catch (e) {
|
|
data = { errors: [error] } as KyooErrors;
|
|
}
|
|
console.log(
|
|
`Invalid response (${
|
|
"method" in context && context.method ? context.method : "GET"
|
|
} ${path}):`,
|
|
data,
|
|
resp.status,
|
|
);
|
|
throw data as KyooErrors;
|
|
}
|
|
|
|
// If the method is DELETE, 204 NoContent is returned from kyoo.
|
|
// @ts-ignore
|
|
if (context.method === "DELETE") return undefined;
|
|
|
|
let data;
|
|
try {
|
|
data = await resp.json();
|
|
} catch (e) {
|
|
console.error("Invald json from kyoo", e);
|
|
throw { errors: ["Invalid repsonse from kyoo"] };
|
|
}
|
|
if (!type) return data;
|
|
const parsed = await type.safeParseAsync(data);
|
|
if (!parsed.success) {
|
|
console.log("Path: ", path, " Response: ", resp.status, " Parse error: ", parsed.error);
|
|
throw {
|
|
errors: [
|
|
"Invalid response from kyoo. Possible version mismatch between the server and the application.",
|
|
],
|
|
} as KyooErrors;
|
|
}
|
|
return parsed.data;
|
|
};
|
|
|
|
export const createQueryClient = () =>
|
|
new QueryClient({
|
|
defaultOptions: {
|
|
queries: {
|
|
staleTime: Infinity,
|
|
refetchOnWindowFocus: false,
|
|
refetchOnReconnect: false,
|
|
retry: false,
|
|
},
|
|
},
|
|
});
|
|
|
|
export type QueryIdentifier<T = unknown, Ret = T> = {
|
|
parser: z.ZodType<T, z.ZodTypeDef, any>;
|
|
path: (string | undefined)[];
|
|
params?: { [query: string]: boolean | number | string | string[] | undefined };
|
|
infinite?: boolean | { value: true; map?: (x: any[]) => Ret[] };
|
|
/**
|
|
* A custom get next function if the infinite query is not a page.
|
|
*/
|
|
getNext?: (item: unknown) => string | undefined;
|
|
};
|
|
|
|
export type QueryPage<Props = {}, Items = unknown> = ComponentType<
|
|
Props & { randomItems: Items[] }
|
|
> & {
|
|
getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier<any>[];
|
|
getLayout?:
|
|
| QueryPage<{ page: ReactElement }>
|
|
| { Layout: QueryPage<{ page: ReactElement }>; props: object };
|
|
randomItems?: Items[];
|
|
};
|
|
|
|
const toQueryKey = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
|
const prefix = Platform.OS !== "web" ? [kyooApiUrl] : [""];
|
|
|
|
if (query.params) {
|
|
return [
|
|
...prefix,
|
|
...query.path,
|
|
"?" +
|
|
Object.entries(query.params)
|
|
.filter(([_, v]) => v !== undefined)
|
|
.map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
|
|
.join("&"),
|
|
];
|
|
} else {
|
|
return [...prefix, ...query.path];
|
|
}
|
|
};
|
|
|
|
export const useFetch = <Data,>(query: QueryIdentifier<Data>) => {
|
|
return useQuery<Data, KyooErrors>({
|
|
queryKey: toQueryKey(query),
|
|
queryFn: (ctx) => queryFn(ctx, query.parser),
|
|
});
|
|
};
|
|
|
|
export const useInfiniteFetch = <Data, Ret>(query: QueryIdentifier<Data, Ret>) => {
|
|
if (query.getNext) {
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const ret = useInfiniteQuery<Data[], KyooErrors>({
|
|
queryKey: toQueryKey(query),
|
|
queryFn: (ctx) => queryFn(ctx, z.array(query.parser)),
|
|
getNextPageParam: query.getNext,
|
|
initialPageParam: undefined,
|
|
});
|
|
return { ...ret, items: ret.data?.pages.flatMap((x) => x) as unknown as Ret[] | undefined };
|
|
}
|
|
// eslint-disable-next-line react-hooks/rules-of-hooks
|
|
const ret = useInfiniteQuery<Page<Data>, KyooErrors>({
|
|
queryKey: toQueryKey(query),
|
|
queryFn: (ctx) => queryFn(ctx, Paged(query.parser)),
|
|
getNextPageParam: (page: Page<Data>) => page?.next || undefined,
|
|
initialPageParam: undefined,
|
|
});
|
|
const items = ret.data?.pages.flatMap((x) => x.items);
|
|
return {
|
|
...ret,
|
|
items:
|
|
items && typeof query.infinite === "object" && query.infinite.map
|
|
? query.infinite.map(items)
|
|
: (items as unknown as Ret[] | undefined),
|
|
};
|
|
};
|
|
|
|
export const fetchQuery = async (queries: QueryIdentifier[], authToken?: string | null) => {
|
|
// we can't put this check in a function because we want build time optimizations
|
|
// see https://github.com/vercel/next.js/issues/5354 for details
|
|
if (typeof window !== "undefined") return {};
|
|
|
|
const client = createQueryClient();
|
|
await Promise.all(
|
|
queries.map((query) => {
|
|
if (query.infinite) {
|
|
return client.prefetchInfiniteQuery({
|
|
queryKey: toQueryKey(query),
|
|
queryFn: (ctx) => queryFn(ctx, Paged(query.parser), authToken),
|
|
initialPageParam: undefined,
|
|
});
|
|
} else {
|
|
return client.prefetchQuery({
|
|
queryKey: toQueryKey(query),
|
|
queryFn: (ctx) => queryFn(ctx, query.parser, authToken),
|
|
});
|
|
}
|
|
}),
|
|
);
|
|
return dehydrate(client);
|
|
};
|