diff --git a/front/Dockerfile b/front/Dockerfile
index c341b8ce..6ff38b3a 100644
--- a/front/Dockerfile
+++ b/front/Dockerfile
@@ -8,6 +8,7 @@ COPY apps/web/package.json apps/web/package.json
COPY apps/mobile/package.json apps/mobile/package.json
COPY packages/ui/package.json packages/ui/package.json
COPY packages/primitives/package.json packages/primitives/package.json
+COPY packages/models/package.json packages/models/package.json
RUN yarn --immutable
COPY . .
diff --git a/front/Dockerfile.dev b/front/Dockerfile.dev
index 25657fef..b1e58e67 100644
--- a/front/Dockerfile.dev
+++ b/front/Dockerfile.dev
@@ -8,6 +8,7 @@ COPY apps/web/package.json apps/web/package.json
COPY apps/mobile/package.json apps/mobile/package.json
COPY packages/ui/package.json packages/ui/package.json
COPY packages/primitives/package.json packages/primitives/package.json
+COPY packages/models/package.json packages/models/package.json
RUN yarn --immutable
ENV NEXT_TELEMETRY_DISABLED 1
diff --git a/front/apps/mobile/metro.config.js b/front/apps/mobile/metro.config.js
index 36560b59..33d774e3 100644
--- a/front/apps/mobile/metro.config.js
+++ b/front/apps/mobile/metro.config.js
@@ -1,4 +1,23 @@
-// Learn more https://docs.expo.dev/guides/monorepos
+/*
+ * 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 .
+ */
+
const { getDefaultConfig } = require("expo/metro-config");
const path = require("path");
diff --git a/front/apps/web/next.config.js b/front/apps/web/next.config.js
index 320870e3..be7877a8 100755
--- a/front/apps/web/next.config.js
+++ b/front/apps/web/next.config.js
@@ -81,6 +81,7 @@ const nextConfig = {
transpilePackages: [
"@kyoo/ui",
"@kyoo/primitives",
+ "@kyoo/models",
"solito",
"react-native",
"react-native-web",
diff --git a/front/apps/web/package.json b/front/apps/web/package.json
index 61086978..dcd0bfae 100644
--- a/front/apps/web/package.json
+++ b/front/apps/web/package.json
@@ -14,12 +14,12 @@
"@emotion/react": "^11.9.3",
"@emotion/styled": "^11.9.3",
"@jellyfin/libass-wasm": "^4.1.1",
+ "@kyoo/models": "workspace:^",
"@kyoo/primitives": "workspace:^",
"@kyoo/ui": "workspace:^",
"@mui/icons-material": "^5.8.4",
"@mui/material": "^5.8.7",
"@mui/system": "^5.10.10",
- "@tanstack/react-query": "^4.18.0",
"clsx": "^1.2.1",
"csstype": "^3.1.1",
"hls.js": "^1.2.8",
diff --git a/front/apps/web/src/models.ts b/front/apps/web/src/models.ts
new file mode 100644
index 00000000..e451d54b
--- /dev/null
+++ b/front/apps/web/src/models.ts
@@ -0,0 +1,21 @@
+/*
+ * 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 .
+ */
+
+export * from "@kyoo/models";
diff --git a/front/apps/web/src/pages/browse/index.tsx b/front/apps/web/src/pages/browse/index.tsx
index b6d62b0e..85c20a2c 100644
--- a/front/apps/web/src/pages/browse/index.tsx
+++ b/front/apps/web/src/pages/browse/index.tsx
@@ -39,7 +39,7 @@ import { ErrorPage } from "~/components/errors";
import { Navbar } from "@kyoo/ui";
import { Poster, Image } from "@kyoo/primitives";
import { ItemType, LibraryItem, LibraryItemP } from "~/models";
-import { getDisplayDate } from "~/models/utils";
+import { getDisplayDate } from "@kyoo/models";
import { InfiniteScroll } from "~/utils/infinite-scroll";
import { Link } from "~/utils/link";
import { withRoute } from "~/utils/router";
diff --git a/front/apps/web/src/utils/query.ts b/front/apps/web/src/utils/query.ts
index 2808d832..806c6d89 100644
--- a/front/apps/web/src/utils/query.ts
+++ b/front/apps/web/src/utils/query.ts
@@ -28,7 +28,7 @@ import {
} from "@tanstack/react-query";
import { z } from "zod";
import { KyooErrors, Page } from "~/models";
-import { Paged } from "~/models/page";
+import { Paged } from "~/models";
const queryFn = async (
type: z.ZodType,
diff --git a/front/packages/models/package.json b/front/packages/models/package.json
new file mode 100644
index 00000000..c0adccba
--- /dev/null
+++ b/front/packages/models/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@kyoo/models",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "packageManager": "yarn@3.2.4",
+ "devDependencies": {
+ "@types/react": "^18.0.25",
+ "typescript": "^4.9.3"
+ },
+ "peerDependencies": {
+ "react": "*",
+ "react-native": "*"
+ },
+ "peerDependenciesMeta": {
+ "react-native-web": {
+ "optional": true
+ }
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^4.18.0",
+ "zod": "^3.19.1"
+ }
+}
diff --git a/front/apps/web/src/models/index.ts b/front/packages/models/src/index.ts
similarity index 94%
rename from front/apps/web/src/models/index.ts
rename to front/packages/models/src/index.ts
index 4932e1fc..025a2157 100644
--- a/front/apps/web/src/models/index.ts
+++ b/front/packages/models/src/index.ts
@@ -20,5 +20,8 @@
export * from "./page";
export * from "./kyoo-errors";
+export * from "./utils"
export * from "./traits";
export * from "./resources";
+
+export * from "./query";
diff --git a/front/apps/web/src/models/kyoo-errors.ts b/front/packages/models/src/kyoo-errors.ts
similarity index 100%
rename from front/apps/web/src/models/kyoo-errors.ts
rename to front/packages/models/src/kyoo-errors.ts
diff --git a/front/apps/web/src/models/page.ts b/front/packages/models/src/page.ts
similarity index 100%
rename from front/apps/web/src/models/page.ts
rename to front/packages/models/src/page.ts
diff --git a/front/packages/models/src/query.tsx b/front/packages/models/src/query.tsx
new file mode 100644
index 00000000..74b58d75
--- /dev/null
+++ b/front/packages/models/src/query.tsx
@@ -0,0 +1,207 @@
+/*
+ * 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 .
+ */
+
+import { ComponentType, ReactElement, ReactNode } from "react";
+import {
+ dehydrate,
+ QueryClient,
+ QueryFunctionContext,
+ useInfiniteQuery,
+ useQuery,
+} from "@tanstack/react-query";
+import { z } from "zod";
+import { KyooErrors } from "./kyoo-errors";
+import { Page, Paged } from "./page";
+import { Library } from "./resources";
+
+const queryFn = async (
+ type: z.ZodType,
+ context: QueryFunctionContext,
+): Promise => {
+ const kyoo_url = process.env.KYOO_URL ?? "http://localhost:5000";
+
+ let resp;
+ try {
+ resp = await fetch(
+ [typeof window === "undefined" ? kyoo_url : "/api"]
+ .concat(
+ context.pageParam ? [context.pageParam] : (context.queryKey.filter((x) => x) as string[]),
+ )
+ .join("/")
+ .replace("/?", "?"),
+ );
+ } 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:", data);
+ throw data as KyooErrors;
+ }
+
+ let data;
+ try {
+ data = await resp.json();
+ } catch (e) {
+ console.error("Invald json from kyoo", e);
+ throw { errors: ["Invalid repsonse from kyoo"] };
+ }
+ const parsed = await type.safeParseAsync(data);
+ if (!parsed.success) {
+ console.log("Parse error: ", parsed.error);
+ throw { errors: parsed.error.errors.map((x) => x.message) } as KyooErrors;
+ }
+ return parsed.data;
+};
+
+export const createQueryClient = () =>
+ new QueryClient({
+ defaultOptions: {
+ queries: {
+ staleTime: Infinity,
+ refetchOnWindowFocus: false,
+ refetchOnReconnect: false,
+ retry: false,
+ },
+ },
+ });
+
+export type QueryIdentifier = {
+ parser: z.ZodType;
+ path: (string | undefined)[];
+ params?: { [query: string]: boolean | number | string | string[] | undefined };
+ infinite?: boolean;
+};
+
+export type QueryPage = ComponentType & {
+ getFetchUrls?: (route: { [key: string]: string }) => QueryIdentifier[];
+ getLayout?: (page: ReactElement) => ReactNode;
+};
+
+const toQueryKey = (query: QueryIdentifier) => {
+ if (query.params) {
+ return [
+ ...query.path,
+ "?" +
+ Object.entries(query.params)
+ .filter(([_, v]) => v !== undefined)
+ .map(([k, v]) => `${k}=${Array.isArray(v) ? v.join(",") : v}`)
+ .join("&"),
+ ];
+ } else {
+ return query.path;
+ }
+};
+
+export const useFetch = (query: QueryIdentifier) => {
+ return useQuery({
+ queryKey: toQueryKey(query),
+ queryFn: (ctx) => queryFn(query.parser, ctx),
+ });
+};
+
+export const useInfiniteFetch = (query: QueryIdentifier) => {
+ const ret = useInfiniteQuery, KyooErrors>({
+ queryKey: toQueryKey(query),
+ queryFn: (ctx) => queryFn(Paged(query.parser), ctx),
+ getNextPageParam: (page: Page) => page?.next || undefined,
+ });
+ return { ...ret, items: ret.data?.pages.flatMap((x) => x.items) };
+};
+
+export const fetchQuery = async (queries: QueryIdentifier[]) => {
+ // 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(Paged(query.parser), ctx),
+ });
+ } else {
+ return client.prefetchQuery({
+ queryKey: toQueryKey(query),
+ queryFn: (ctx) => queryFn(query.parser, ctx),
+ });
+ }
+ }),
+ );
+ return dehydrate(client);
+};
+
+/* export const Fetch = ({ */
+/* query, */
+/* children, */
+/* }: { */
+/* query: QueryIdentifier; */
+/* children: ( */
+/* item: (Data & { isLoading: false }) | { isLoading: true }, */
+/* i: number, */
+/* ) => JSX.Element | null; */
+/* }) => { */
+/* const { data, error, isSuccess, isError } = useFetch(query); */
+
+/* return children(isSuccess ? { ...data, isLoading: false } : { isLoading: true }, 0); */
+/* }; */
+
+type WithLoading- = (Item & { isLoading: false }) | { isLoading: true };
+
+const isPage = (obj: unknown): obj is Page =>
+ (typeof obj === "object" && obj && "items" in obj) || false;
+
+export const Fetch = ({
+ query,
+ placeholderCount,
+ children,
+}: {
+ query: QueryIdentifier;
+ placeholderCount?: number;
+ children: (
+ item: Data extends Page ? WithLoading
- : WithLoading,
+ i: number,
+ ) => JSX.Element | null;
+}) => {
+ const { data, error } = useFetch(query);
+
+ if (error) throw error;
+ if (!isPage