Repository: IgnisDa/ryot (language: rust)

Issue:
[FEATURE REQUEST] - Make metadata editeable
Hello.

I would like the metadata of at least custom items you create to be editeable after it has been created.
It would also be nice to be able to locally change the chosen images that show up for the items that are searched once they are added to your list.
Or to be able to add an author's name to books that are currently missing it for example.

Thank you :)


Discussion / hints:
> Or to be able to add an author's name to books that are currently missing it for example.

If you are using Openlibrary, then I suggest adding that data to openlibrary itself. That way everyone can benefit from it.

> It would also be nice to be able to locally change the chosen images that show up for the items that are searched once they are added to your list.

Can you explain what you mean by this?
I'd also like to add to this, re: cover image. When pulling book metadata, there are multiple covers I can manually cycle through. But I cannot change which one I want to be the main one. It's always the first, lowest quality one that's very pixelated and blurry.
@earthly-alien which provider is this happenning for (eg. tmdb, openlibrary etc)? I have code which orders images by their quality but i have not applied it to all providers it seems.
Openlibrary and Google Books. So just the book metadata providers.
@IgnisDa 
> Can you explain what you mean by this?

Sometimes the official artwork of some games or movies can be a bit intense if i'm not mentally ready, especially if it's related to one of my phobias so being able to locally alter the thumbnail image to be of a different poster or a custom screenshot would be helpful :)
At the moment i'm adding the games i'm tracking so i'm like having to actively navigate carefully and avoid looking at some of the titles which is a bit annoying lol
Unfortunately I won't be providing a way to edit non custom metadata. It opens a whole can of worms like keeping stuff in sync/overwriting changes/user permissions that I don't won't to deal with.

I'd recommend just creating custom metadata for your use case. I'll provide a way to edit them soon.

Fix patch:
diff --git a/Cargo.lock b/Cargo.lock
index 1e6811a658..83a507ef59 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1345,6 +1345,7 @@ dependencies = [
  "env-utils",
  "reqwest 0.12.9",
  "serde",
+ "tokio",
 ]
 
 [[package]]
@@ -1771,6 +1772,8 @@ dependencies = [
  "rust_decimal_macros",
  "sea-orm",
  "serde",
+ "serde_json",
+ "sqlx",
  "supporting-service",
  "tracing",
  "traits",
@@ -2377,6 +2380,7 @@ dependencies = [
  "dependent-utils",
  "enum-models",
  "fitness-models",
+ "futures",
  "migrations",
  "nanoid",
  "reqwest 0.12.9",
@@ -3848,7 +3852,6 @@ dependencies = [
  "serde",
  "serde_json",
  "supporting-service",
- "tokio",
  "tracing",
  "traits",
  "user-models",
@@ -5889,12 +5892,12 @@ name = "specific-utils"
 version = "0.1.0"
 dependencies = [
  "anyhow",
+ "common-utils",
  "database-models",
  "dependent-utils",
  "enum-models",
  "sea-orm",
  "supporting-service",
- "tokio",
 ]
 
 [[package]]
@@ -7049,7 +7052,6 @@ dependencies = [
  "dependent-utils",
  "enum-models",
  "enum_meta",
- "fitness-models",
  "itertools 0.13.0",
  "jwt-service",
  "media-models",
@@ -7057,7 +7059,6 @@ dependencies = [
  "notification-service",
  "openidconnect",
  "sea-orm",
- "serde_json",
  "supporting-service",
  "tracing",
  "user-models",
diff --git a/Cargo.toml b/Cargo.toml
index e9361de94d..fc18019725 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -144,6 +144,7 @@ serde_with = { version = "=3.11.0", features = ["chrono_0_4"] }
 serde-xml-rs = "=0.6.0"
 slug = "=0.1.6"
 sonarr-api-rs = "=3.0.0"
+sqlx = { version = "=0.8.2", default-features = false, features = ["postgres"] }
 strum = { version = "=0.26.3", features = ["derive"] }
 struson = { version = "=0.6.0", features = ["serde"] }
 reqwest = { version = "=0.12.9", features = ["json", "stream"] }
diff --git a/apps/frontend/app/components/common.tsx b/apps/frontend/app/components/common.tsx
index bad73d5629..3899604062 100644
--- a/apps/frontend/app/components/common.tsx
+++ b/apps/frontend/app/components/common.tsx
@@ -73,6 +73,7 @@ import {
 	convertDecimalToThreePointSmiley,
 	dayjsLib,
 	getSurroundingElements,
+	openConfirmationModal,
 	reviewYellow,
 } from "~/lib/generals";
 import {
@@ -88,7 +89,6 @@ import {
 import { useReviewEntity } from "~/lib/state/media";
 import type { action } from "~/routes/actions";
 import classes from "~/styles/common.module.css";
-import { confirmWrapper } from "./confirmation";
 import {
 	ExerciseDisplayItem,
 	WorkoutDisplayItem,
@@ -541,24 +541,24 @@ export const ReviewItemDisplay = (props: {
 									<IconEdit size={16} />
 								</ActionIcon>
 								<ActionIcon
-									onClick={async () => {
-										const conf = await confirmWrapper({
-											confirmation:
-												"Are you sure you want to delete this review? This action cannot be undone.",
-										});
-										if (conf)
-											deleteReviewFetcher.submit(
-												{
-													shouldDelete: "true",
-													reviewId: props.review.id || null,
-												},
-												{
-													method: "post",
-													action: $path("/actions", {
-														intent: "performReviewAction",
-													}),
-												},
-											);
+									onClick={() => {
+										openConfirmationModal(
+											"Are you sure you want to delete this review? This action cannot be undone.",
+											() => {
+												deleteReviewFetcher.submit(
+													{
+														shouldDelete: "true",
+														reviewId: props.review.id || null,
+													},
+													{
+														method: "post",
+														action: $path("/actions", {
+															intent: "performReviewAction",
+														}),
+													},
+												);
+											},
+										);
 									}}
 									color="red"
 								>
@@ -732,14 +732,13 @@ export const ReviewItemDisplay = (props: {
 															<ActionIcon
 																color="red"
 																type="submit"
-																onClick={async (e) => {
+																onClick={(e) => {
 																	const form = e.currentTarget.form;
 																	e.preventDefault();
-																	const conf = await confirmWrapper({
-																		confirmation:
-																			"Are you sure you want to delete this comment?",
-																	});
-																	if (conf && form) submit(form);
+																	openConfirmationModal(
+																		"Are you sure you want to delete this comment?",
+																		() => submit(form),
+																	);
 																}}
 															>
 																<IconTrash size={16} />
@@ -886,14 +885,13 @@ export const DisplayCollection = (props: {
 					/>
 					<ActionIcon
 						size={16}
-						onClick={async (e) => {
+						onClick={(e) => {
 							const form = e.currentTarget.form;
 							e.preventDefault();
-							const conf = await confirmWrapper({
-								confirmation:
-									"Are you sure you want to remove this media from this collection?",
-							});
-							if (conf && form) submit(form);
+							openConfirmationModal(
+								"Are you sure you want to remove this media from this collection?",
+								() => submit(form),
+							);
 						}}
 					>
 						<IconX />
diff --git a/apps/frontend/app/components/confirmation.tsx b/apps/frontend/app/components/confirmation.tsx
deleted file mode 100644
index dd9a55fd16..0000000000
--- a/apps/frontend/app/components/confirmation.tsx
+++ /dev/null
@@ -1,59 +0,0 @@
-import { Button, Group, Modal, Stack, Text } from "@mantine/core";
-import {
-	type ConfirmDialogProps,
-	confirmable,
-	createConfirmationCreater,
-	createMountPoint,
-	createReactTreeMounter,
-} from "react-confirm";
-
-type ConfirmationProps = {
-	confirmation: string;
-	title?: string;
-	okLabel?: string;
-	cancelLabel?: string;
-};
-
-type ConfirmationResponse = boolean;
-
-const Confirmation: React.FC<
-	ConfirmDialogProps<ConfirmationProps, ConfirmationResponse>
-> = (props) => {
-	return (
-		<Modal
-			opened={props.show}
-			onClose={() => props.proceed(false)}
-			size="sm"
-			centered
-			withCloseButton={false}
-			title={props.title}
-		>
-			<Stack>
-				<Text>{props.confirmation}</Text>
-				<Group justify="space-between">
-					<Button onClick={() => props.proceed(false)} variant="outline">
-						{props.cancelLabel ?? "Cancel"}
-					</Button>
-					<Button onClick={() => props.proceed(true)} color="red">
-						{props.okLabel ?? "OK"}
-					</Button>
-				</Group>
-			</Stack>
-		</Modal>
-	);
-};
-
-const AppConfirmable = confirmable<ConfirmationProps, ConfirmationResponse>(
-	Confirmation,
-);
-
-const mounter = createReactTreeMounter();
-
-export const createConfirmation = createConfirmationCreater(mounter);
-
-export const ConfirmationMountPoint = createMountPoint(mounter);
-
-export const confirmWrapper = createConfirmation<
-	ConfirmationProps,
-	ConfirmationResponse
->(AppConfirmable);
diff --git a/apps/frontend/app/components/media.tsx b/apps/frontend/app/components/media.tsx
index 2e66aaa9fc..636dc21919 100644
--- a/apps/frontend/app/components/media.tsx
+++ b/apps/frontend/app/components/media.tsx
@@ -40,10 +40,10 @@ import {
 	DisplayThreePointReview,
 	MEDIA_DETAILS_HEIGHT,
 } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	clientGqlService,
 	getPartialMetadataDetailsQuery,
+	openConfirmationModal,
 	queryFactory,
 	reviewYellow,
 } from "~/lib/generals";
@@ -397,14 +397,12 @@ export const ToggleMediaMonitorMenuItem = (props: {
 					const form = e.currentTarget.form;
 					if (form) {
 						e.preventDefault();
-						if (isMonitored) {
-							const conf = await confirmWrapper({
-								confirmation: "Are you sure you want to stop monitoring?",
-							});
-							if (conf) submit(form);
-						} else {
-							submit(form);
-						}
+						if (isMonitored)
+							openConfirmationModal(
+								"Are you sure you want to stop monitoring?",
+								() => submit(form),
+							);
+						else submit(form);
 					}
 				}}
 			>
diff --git a/apps/frontend/app/lib/generals.ts b/apps/frontend/app/lib/generals.tsx
similarity index 96%
rename from apps/frontend/app/lib/generals.ts
rename to apps/frontend/app/lib/generals.tsx
index 9fced0afcc..135365904c 100644
--- a/apps/frontend/app/lib/generals.ts
+++ b/apps/frontend/app/lib/generals.tsx
@@ -3,7 +3,8 @@ import {
 	createQueryKeys,
 	mergeQueryKeys,
 } from "@lukemorales/query-key-factory";
-import type { MantineColor } from "@mantine/core";
+import { type MantineColor, Text } from "@mantine/core";
+import { modals } from "@mantine/modals";
 import {
 	MediaLot,
 	MediaSource,
@@ -306,10 +307,10 @@ export const getStringAsciiValue = (input: string) => {
 	return total;
 };
 
-export const selectRandomElement = <T>(array: T[], input: string): T => {
+export function selectRandomElement<T>(array: T[], input: string): T {
 	// taken from https://stackoverflow.com/questions/44975435/using-mod-operator-in-javascript-to-wrap-around#comment76926119_44975435
 	return array[(getStringAsciiValue(input) + array.length) % array.length];
-};
+}
 
 export const getMetadataIcon = (lot: MediaLot) =>
 	match(lot)
@@ -338,16 +339,16 @@ export const clientGqlService = new GraphQLClient(
 	},
 );
 
-export const getSurroundingElements = <T>(
+export function getSurroundingElements<T>(
 	array: Array<T>,
 	elementIndex: number,
-): Array<number> => {
+): Array<number> {
 	if (array.length === 1) return [0];
 	const lastIndex = array.length - 1;
 	if (elementIndex === 0) return [lastIndex, elementIndex, elementIndex + 1];
 	if (elementIndex === lastIndex) return [elementIndex - 1, elementIndex, 0];
 	return [elementIndex - 1, elementIndex, elementIndex + 1];
-};
+}
 
 const mediaQueryKeys = createQueryKeys("media", {
 	metadataPartialDetails: (metadataId: string) => ({
@@ -488,3 +489,11 @@ export const MediaColors: EntityColor = {
 	REVIEW: "green.5",
 	USER_MEASUREMENT: "indigo",
 };
+
+export const openConfirmationModal = (title: string, onConfirm: () => void) =>
+	modals.openConfirmModal({
+		title: "Confirmation",
+		onConfirm: onConfirm,
+		children: <Text size="sm">{title}</Text>,
+		labels: { confirm: "Confirm", cancel: "Cancel" },
+	});
diff --git a/apps/frontend/app/lib/hooks.ts b/apps/frontend/app/lib/hooks.ts
index b49371d6b9..f7450bed03 100644
--- a/apps/frontend/app/lib/hooks.ts
+++ b/apps/frontend/app/lib/hooks.ts
@@ -1,6 +1,7 @@
 import { useComputedColorScheme, useMantineTheme } from "@mantine/core";
-import { useForceUpdate, useListState } from "@mantine/hooks";
+import { useForceUpdate } from "@mantine/hooks";
 import {
+	useNavigate,
 	useRevalidator,
 	useRouteLoaderData,
 	useSearchParams,
@@ -86,7 +87,8 @@ export const useAppSearchParam = (cookieKey: string) => {
 
 export const useConfirmSubmit = () => {
 	const submit = useSubmit();
-	const fn = (e: FormEvent<HTMLFormElement> | HTMLFormElement) => {
+	const fn = (e: FormEvent<HTMLFormElement> | HTMLFormElement | null) => {
+		if (!e) return;
 		if (e.preventDefault) e.preventDefault();
 		submit(e.currentTarget || e, { navigate: false });
 	};
@@ -94,12 +96,13 @@ export const useConfirmSubmit = () => {
 };
 
 export const useGetWorkoutStarter = () => {
+	const navigate = useNavigate();
 	const revalidator = useRevalidator();
 	const [_, setCurrentWorkout] = useCurrentWorkout();
 
 	const fn = (wkt: InProgressWorkout, action: FitnessAction) => {
 		setCurrentWorkout(wkt);
-		window.location.href = $path("/fitness/:action", { action });
+		navigate($path("/fitness/:action", { action }), { flushSync: true });
 		revalidator.revalidate();
 	};
 	return fn;
@@ -187,22 +190,6 @@ export const useGetWatchProviders = (mediaLot: MediaLot) => {
 	return watchProviders;
 };
 
-export const useComplexJsonUpdate = () => {
-	const [toUpdatePreferences, updateUserPreferencesHandler] = useListState<
-		[string, string]
-	>([]);
-
-	const reset = () => updateUserPreferencesHandler.setState([]);
-
-	const appendPref = (property: string, value: string) => {
-		const index = toUpdatePreferences.findIndex((p) => p[0] === property);
-		if (index !== -1) updateUserPreferencesHandler.remove(index);
-		updateUserPreferencesHandler.append([property, value]);
-	};
-
-	return { reset, appendPref, toUpdatePreferences };
-};
-
 export const useIsFitnessActionActive = () => {
 	const [currentWorkout] = useCurrentWorkout();
 	const action = currentWorkout?.currentActionOrCompleted;
diff --git a/apps/frontend/app/lib/state/fitness.ts b/apps/frontend/app/lib/state/fitness.ts
index 0f51dfffbb..5db3c404c1 100644
--- a/apps/frontend/app/lib/state/fitness.ts
+++ b/apps/frontend/app/lib/state/fitness.ts
@@ -112,7 +112,7 @@ export const useGetSetAtIndex = (exerciseIdx: number, setIdx: number) => {
 export const getDefaultWorkout = (
 	fitnessEntity: FitnessAction,
 ): InProgressWorkout => {
-	const date = dayjsLib().add(4, "second");
+	const date = dayjsLib().add(3, "second");
 	return {
 		images: [],
 		videos: [],
diff --git a/apps/frontend/app/root.tsx b/apps/frontend/app/root.tsx
index d6d855ae86..455856bbba 100644
--- a/apps/frontend/app/root.tsx
+++ b/apps/frontend/app/root.tsx
@@ -13,6 +13,7 @@ import "@mantine/charts/styles.css";
 import "@mantine/carousel/styles.css";
 import "@mantine/dates/styles.css";
 import "@mantine/notifications/styles.css";
+import { ModalsProvider } from "@mantine/modals";
 import {
 	type LinksFunction,
 	type LoaderFunctionArgs,
@@ -31,7 +32,6 @@ import {
 import { QueryClientProvider } from "@tanstack/react-query";
 import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
 import "mantine-datatable/styles.layer.css";
-import { ConfirmationMountPoint } from "~/components/confirmation";
 import { Toaster } from "~/components/toaster";
 import { LOGO_IMAGE_URL, queryClient } from "~/lib/generals";
 import {
@@ -129,23 +129,24 @@ export default function App() {
 						classNamesPrefix="mnt"
 						forceColorScheme={loaderData.defaultColorScheme}
 					>
-						<ConfirmationMountPoint />
-						{["loading", "submitting"].includes(navigation.state) ? (
-							<Loader
-								top={10}
-								size="sm"
-								right={10}
-								pos="fixed"
-								color="yellow"
-								style={{ zIndex: 10 }}
-							/>
-						) : null}
-						<Toaster toast={loaderData.toast} />
-						<Flex style={{ flexGrow: 1 }} mih="100vh">
-							<Outlet />
-						</Flex>
-						<ScrollRestoration />
-						<Scripts />
+						<ModalsProvider>
+							{["loading", "submitting"].includes(navigation.state) ? (
+								<Loader
+									top={10}
+									size="sm"
+									right={10}
+									pos="fixed"
+									color="yellow"
+									style={{ zIndex: 10 }}
+								/>
+							) : null}
+							<Toaster toast={loaderData.toast} />
+							<Flex style={{ flexGrow: 1 }} mih="100vh">
+								<Outlet />
+							</Flex>
+							<ScrollRestoration />
+							<Scripts />
+						</ModalsProvider>
 					</MantineProvider>
 					<ReactQueryDevtools buttonPosition="top-right" />
 				</QueryClientProvider>
diff --git a/apps/frontend/app/routes/_dashboard._index.tsx b/apps/frontend/app/routes/_dashboard._index.tsx
index c9391b247c..4eca679b8d 100644
--- a/apps/frontend/app/routes/_dashboard._index.tsx
+++ b/apps/frontend/app/routes/_dashboard._index.tsx
@@ -1,18 +1,23 @@
 import {
+	ActionIcon,
 	Alert,
 	Box,
 	Center,
 	Container,
 	Flex,
+	Group,
 	RingProgress,
 	SimpleGrid,
 	Stack,
 	Text,
-	Title,
 	useMantineTheme,
 } from "@mantine/core";
-import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node";
-import { Link, useLoaderData } from "@remix-run/react";
+import type {
+	ActionFunctionArgs,
+	LoaderFunctionArgs,
+	MetaArgs,
+} from "@remix-run/node";
+import { Form, Link, useLoaderData } from "@remix-run/react";
 import {
 	type CalendarEventPartFragment,
 	CollectionContentsDocument,
@@ -20,6 +25,7 @@ import {
 	DashboardElementLot,
 	GraphqlSortOrder,
 	MediaLot,
+	RefreshUserRecommendationsKeyDocument,
 	UserAnalyticsDocument,
 	type UserPreferences,
 	UserRecommendationsDocument,
@@ -27,6 +33,7 @@ import {
 } from "@ryot/generated/graphql/backend/graphql";
 import {
 	formatQuantityWithCompactNotation,
+	getActionIntent,
 	humanizeDuration,
 	isNumber,
 } from "@ryot/ts-utils";
@@ -34,6 +41,7 @@ import {
 	IconBarbell,
 	IconFriends,
 	IconInfoCircle,
+	IconRotateClockwise,
 	IconScaleOutline,
 	IconServer,
 } from "@tabler/icons-react";
@@ -43,13 +51,21 @@ import { $path } from "remix-routes";
 import { ClientOnly } from "remix-utils/client-only";
 import invariant from "tiny-invariant";
 import { match } from "ts-pattern";
+import { withQuery } from "ufo";
 import { useLocalStorage } from "usehooks-ts";
 import { ApplicationGrid, ProRequiredAlert } from "~/components/common";
 import { DisplayCollectionEntity } from "~/components/common";
 import { displayWeightWithUnit } from "~/components/fitness";
 import { MetadataDisplayItem } from "~/components/media";
-import { MediaColors, dayjsLib, getLot, getMetadataIcon } from "~/lib/generals";
 import {
+	MediaColors,
+	dayjsLib,
+	getLot,
+	getMetadataIcon,
+	openConfirmationModal,
+} from "~/lib/generals";
+import {
+	useConfirmSubmit,
 	useCoreDetails,
 	useGetMantineColors,
 	useUserPreferences,
@@ -130,15 +146,30 @@ export const meta = (_args: MetaArgs<typeof loader>) => {
 	return [{ title: "Home | Ryot" }];
 };
 
+export const action = async ({ request }: ActionFunctionArgs) => {
+	const intent = getActionIntent(request);
+	return await match(intent)
+		.with("refreshUserRecommendationsKey", async () => {
+			await serverGqlService.authenticatedRequest(
+				request,
+				RefreshUserRecommendationsKeyDocument,
+				{},
+			);
+			return {};
+		})
+		.run();
+};
+
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const coreDetails = useCoreDetails();
 	const userPreferences = useUserPreferences();
 	const unitSystem = useUserUnitSystem();
 	const theme = useMantineTheme();
-	const latestUserSummary = loaderData.userAnalytics.activities.items.at(0);
+	const submit = useConfirmSubmit();
 
 	const dashboardMessage = coreDetails.frontend.dashboardMessage;
+	const latestUserSummary = loaderData.userAnalytics.activities.items.at(0);
 
 	const [isAlertDismissed, setIsAlertDismissed] = useLocalStorage(
 		`AlertDismissed-${CryptoJS.SHA256(dashboardMessage)}`,
@@ -167,7 +198,7 @@ export default function Page() {
 						.with([DashboardElementLot.Upcoming, false], ([v, _]) =>
 							loaderData.userUpcomingCalendarEvents.length > 0 ? (
 								<Section key={v} lot={v}>
-									<Title>Upcoming</Title>
+									<SectionTitle text="Upcoming" />
 									<ApplicationGrid>
 										{loaderData.userUpcomingCalendarEvents.map((um) => (
 											<UpComingMedia um={um} key={um.calendarEventId} />
@@ -180,7 +211,7 @@ export default function Page() {
 							loaderData.inProgressCollectionContents.results.items.length >
 							0 ? (
 								<Section key={v} lot={v}>
-									<Title>In Progress</Title>
+									<SectionTitle text="In Progress" />
 									<ApplicationGrid>
 										{loaderData.inProgressCollectionContents.results.items.map(
 											(lm) => (
@@ -197,7 +228,30 @@ export default function Page() {
 						)
 						.with([DashboardElementLot.Recommendations, false], ([v, _]) => (
 							<Section key={v} lot={v}>
-								<Title>Recommendations</Title>
+								<Group justify="space-between">
+									<SectionTitle text="Recommendations" />
+									<Form
+										method="POST"
+										action={withQuery(".?index", {
+											intent: "refreshUserRecommendationsKey",
+										})}
+									>
+										<ActionIcon
+											type="submit"
+											variant="subtle"
+											onClick={(e) => {
+												const form = e.currentTarget.form;
+												e.preventDefault();
+												openConfirmationModal(
+													"Are you sure you want to refresh the recommendations?",
+													() => submit(form),
+												);
+											}}
+										>
+											<IconRotateClockwise />
+										</ActionIcon>
+									</Form>
+								</Group>
 								{coreDetails.isServerKeyValidated ? (
 									<ApplicationGrid>
 										{loaderData.userRecommendations.map((lm) => (
@@ -212,7 +266,7 @@ export default function Page() {
 						.with([DashboardElementLot.Summary, false], ([v, _]) =>
 							latestUserSummary ? (
 								<Section key={v} lot={v}>
-									<Title>Summary</Title>
+									<SectionTitle text="Summary" />
 									<SimpleGrid
 										cols={{ base: 1, sm: 2, md: 3 }}
 										style={{ alignItems: "center" }}
@@ -448,6 +502,12 @@ export default function Page() {
 	);
 }
 
+const SectionTitle = (props: { text: string }) => (
+	<Text fz={{ base: "h2", md: "h1" }} fw="bold">
+		{props.text}
+	</Text>
+);
+
 const UpComingMedia = ({ um }: { um: CalendarEventPartFragment }) => {
 	const today = dayjsLib().startOf("day");
 	const numDaysLeft = dayjsLib(um.date).diff(today, "day");
diff --git a/apps/frontend/app/routes/_dashboard.collections.list.tsx b/apps/frontend/app/routes/_dashboard.collections.list.tsx
index 79c0b3f8af..81f5f513b6 100644
--- a/apps/frontend/app/routes/_dashboard.collections.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.collections.list.tsx
@@ -70,13 +70,13 @@ import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import { DebouncedSearchInput, ProRequiredAlert } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	PRO_REQUIRED_MESSAGE,
 	clientGqlService,
 	commaDelimitedString,
 	dayjsLib,
 	getPartialMetadataDetailsQuery,
+	openConfirmationModal,
 	queryClient,
 	queryFactory,
 } from "~/lib/generals";
@@ -425,14 +425,13 @@ const DisplayCollection = (props: {
 										type="submit"
 										color="red"
 										variant="outline"
-										onClick={async (e) => {
+										onClick={(e) => {
 											const form = e.currentTarget.form;
 											e.preventDefault();
-											const conf = await confirmWrapper({
-												confirmation:
-													"Are you sure you want to delete this collection?",
-											});
-											if (conf && form) submit(form);
+											openConfirmationModal(
+												"Are you sure you want to delete this collection?",
+												() => submit(form),
+											);
 										}}
 									>
 										<IconTrashFilled size={18} />
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
index 7bec5f7cbd..7b287aaf1c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$action.tsx
@@ -98,7 +98,6 @@ import { useInterval, useOnClickOutside } from "usehooks-ts";
 import { v4 as randomUUID } from "uuid";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	DisplaySetStatistics,
 	ExerciseHistory,
@@ -113,6 +112,7 @@ import {
 	getExerciseDetailsPath,
 	getSetColor,
 	getSurroundingElements,
+	openConfirmationModal,
 	postMessageToServiceWorker,
 	queryClient,
 	queryFactory,
@@ -491,7 +491,7 @@ export default function Page() {
 												size="compact-sm"
 												loading={isSaveBtnLoading}
 												disabled={isWorkoutPaused}
-												onClick={async () => {
+												onClick={() => {
 													if (!currentWorkout.name) {
 														notifications.show({
 															color: "red",
@@ -503,73 +503,73 @@ export default function Page() {
 														});
 														return;
 													}
-													const yes = await confirmWrapper({
-														confirmation: loaderData.isCreatingTemplate
+													openConfirmationModal(
+														loaderData.isCreatingTemplate
 															? "Only sets that have data will added. Are you sure you want to save this template?"
 															: "Only sets marked as confirmed will be recorded. Are you sure you want to finish this workout?",
-													});
-													if (yes) {
-														setIsSaveBtnLoading(true);
-														await new Promise((r) => setTimeout(r, 1000));
-														const input = currentWorkoutToCreateWorkoutInput(
-															currentWorkout,
-															loaderData.isCreatingTemplate,
-														);
-														for (const exercise of currentWorkout.exercises) {
-															queryClient.removeQueries({
-																queryKey:
-																	queryFactory.fitness.userExerciseDetails(
-																		exercise.exerciseId,
-																	).queryKey,
-															});
-														}
-														stopTimer();
-														try {
-															const [entityId, fitnessEntity] = await match(
+														async () => {
+															setIsSaveBtnLoading(true);
+															await new Promise((r) => setTimeout(r, 1000));
+															const input = currentWorkoutToCreateWorkoutInput(
+																currentWorkout,
 																loaderData.isCreatingTemplate,
-															)
-																.with(true, () =>
-																	clientGqlService
-																		.request(
-																			CreateOrUpdateUserWorkoutTemplateDocument,
-																			input,
-																		)
-																		.then((c) => [
-																			c.createOrUpdateUserWorkoutTemplate,
-																			FitnessEntity.Templates,
-																		]),
+															);
+															for (const exercise of currentWorkout.exercises) {
+																queryClient.removeQueries({
+																	queryKey:
+																		queryFactory.fitness.userExerciseDetails(
+																			exercise.exerciseId,
+																		).queryKey,
+																});
+															}
+															stopTimer();
+															try {
+																const [entityId, fitnessEntity] = await match(
+																	loaderData.isCreatingTemplate,
 																)
-																.with(false, () =>
-																	clientGqlService
-																		.request(
-																			CreateOrUpdateUserWorkoutDocument,
-																			input,
-																		)
-																		.then((c) => [
-																			c.createOrUpdateUserWorkout,
-																			FitnessEntity.Workouts,
-																		]),
+																	.with(true, () =>
+																		clientGqlService
+																			.request(
+																				CreateOrUpdateUserWorkoutTemplateDocument,
+																				input,
+																			)
+																			.then((c) => [
+																				c.createOrUpdateUserWorkoutTemplate,
+																				FitnessEntity.Templates,
+																			]),
+																	)
+																	.with(false, () =>
+																		clientGqlService
+																			.request(
+																				CreateOrUpdateUserWorkoutDocument,
+																				input,
+																			)
+																			.then((c) => [
+																				c.createOrUpdateUserWorkout,
+																				FitnessEntity.Workouts,
+																			]),
+																	)
+																	.exhaustive();
+																if (
+																	loaderData.action === FitnessAction.LogWorkout
 																)
-																.exhaustive();
-															if (
-																loaderData.action === FitnessAction.LogWorkout
-															)
-																events.createWorkout();
-															setCurrentWorkout(RESET);
-															navigate(
-																$path("/fitness/:entity/:id", {
-																	id: entityId,
-																	entity: fitnessEntity,
-																}),
-															);
-														} catch (e) {
-															notifications.show({
-																color: "red",
-																message: `Error while saving workout: ${JSON.stringify(e)}`,
-															});
-															setIsSaveBtnLoading(false);
-														}
-													}
+																	events.createWorkout();
+																setCurrentWorkout(RESET);
+																navigate(
+																	$path("/fitness/:entity/:id", {
+																		id: entityId,
+																		entity: fitnessEntity,
+																	}),
+																);
+															} catch (e) {
+																notifications.show({
+																	color: "red",
+																	message: `Error while saving workout: ${JSON.stringify(e)}`,
+																});
+																setIsSaveBtnLoading(false);
+															}
+														},
+													);
 												}}
 											>
 												{loaderData.isCreatingTemplate ||
@@ -584,21 +584,21 @@ export default function Page() {
 										variant="subtle"
 										radius="md"
 										size="compact-sm"
-										onClick={async () => {
-											const yes = await confirmWrapper({
-												confirmation: `Are you sure you want to cancel this ${
+										onClick={() => {
+											openConfirmationModal(
+												`Are you sure you want to cancel this ${
 													loaderData.isCreatingTemplate ? "template" : "workout"
 												}?`,
-											});
-											if (yes) {
-												for (const e of currentWorkout.exercises) {
-													const assets = [...e.images, ...e.videos];
-													for (const asset of assets)
-														deleteUploadedAsset(asset.key);
-												}
-												navigate($path("/"), { replace: true });
-												setCurrentWorkout(RESET);
-											}
+												() => {
+													for (const e of currentWorkout.exercises) {
+														const assets = [...e.images, ...e.videos];
+														for (const asset of assets)
+															deleteUploadedAsset(asset.key);
+													}
+													navigate($path("/"), { replace: true });
+													setCurrentWorkout(RESET);
+												},
+											);
 										}}
 									>
 										Cancel
@@ -868,11 +868,11 @@ const ImageDisplay = (props: { imageSrc: string; removeImage: () => void }) => {
 				left={-12}
 				color="red"
 				pos="absolute"
-				onClick={async () => {
-					const yes = await confirmWrapper({
-						confirmation: "Are you sure you want to remove this image?",
-					});
-					if (yes) props.removeImage();
+				onClick={() => {
+					openConfirmationModal(
+						"Are you sure you want to remove this image?",
+						() => props.removeImage(),
+					);
 				}}
 			>
 				<IconTrash />
@@ -1044,18 +1044,18 @@ const EditSupersetModal = (props: {
 				<Button
 					color="red"
 					flex="none"
-					onClick={async () => {
-						const yes = await confirmWrapper({
-							confirmation: "Are you sure you want to delete this superset?",
-						});
-						if (yes) {
-							setCurrentWorkout(
-								produce(cw, (draft) => {
-									draft.supersets.splice(props.superset[0], 1);
-								}),
-							);
-							props.onClose();
-						}
+					onClick={() => {
+						openConfirmationModal(
+							"Are you sure you want to delete this superset?",
+							() => {
+								setCurrentWorkout(
+									produce(cw, (draft) => {
+										draft.supersets.splice(props.superset[0], 1);
+									}),
+								);
+								props.onClose();
+							},
+						);
 					}}
 				>
 					Delete superset
@@ -1108,6 +1108,7 @@ const UploadAssetsModal = (props: {
 	const coreDetails = useCoreDetails();
 	const fileUploadAllowed = coreDetails.fileStorageEnabled;
 	const [currentWorkout, setCurrentWorkout] = useCurrentWorkout();
+	const [isFileUploading, setIsFileUploading] = useState(false);
 
 	if (!currentWorkout) return null;
 
@@ -1120,6 +1121,7 @@ const UploadAssetsModal = (props: {
 			return;
 		}
 		if (!file) return;
+		setIsFileUploading(true);
 		const imageSrc = URL.createObjectURL(file);
 		const toSubmitForm = new FormData();
 		toSubmitForm.append("file", file, "image.jpg");
@@ -1141,6 +1143,8 @@ const UploadAssetsModal = (props: {
 				color: "red",
 				message: "Error while uploading image",
 			});
+		} finally {
+			setIsFileUploading(false);
 		}
 	};
 
@@ -1208,6 +1212,7 @@ const UploadAssetsModal = (props: {
 										flex={1}
 										color="cyan"
 										variant="outline"
+										loading={isFileUploading}
 										leftSection={<IconLibraryPhoto />}
 									>
 										Select picture
@@ -1225,6 +1230,7 @@ const UploadAssetsModal = (props: {
 										flex={1}
 										color="cyan"
 										variant="outline"
+										loading={isFileUploading}
 										leftSection={<IconCamera />}
 									>
 										Take picture
@@ -1450,32 +1456,33 @@ const ExerciseDisplay = (props: {
 							<Menu.Item
 								color="red"
 								leftSection={<IconTrash size={14} />}
-								onClick={async () => {
-									const yes = await confirmWrapper({
-										confirmation: `This removes '${exercise.name}' and all its sets from your workout. You can not undo this action. Are you sure you want to continue?`,
-									});
-									if (yes) {
-										const assets = [...exercise.images, ...exercise.videos];
-										for (const asset of assets) deleteUploadedAsset(asset.key);
-										setCurrentWorkout(
-											produce(currentWorkout, (draft) => {
-												const idx = draft.supersets.findIndex((s) =>
-													s.exercises.includes(exercise.identifier),
-												);
-												if (idx !== -1) {
-													if (draft.supersets[idx].exercises.length === 2)
-														draft.supersets.splice(idx, 1);
-													else
-														draft.supersets[idx].exercises = draft.supersets[
-															idx
-														].exercises.filter(
-															(e) => e !== exercise.identifier,
-														);
-												}
-												draft.exercises.splice(props.exerciseIdx, 1);
-											}),
-										);
-									}
+								onClick={() => {
+									openConfirmationModal(
+										`This removes '${exercise.name}' and all its sets from your workout. You can not undo this action. Are you sure you want to continue?`,
+										() => {
+											const assets = [...exercise.images, ...exercise.videos];
+											for (const asset of assets)
+												deleteUploadedAsset(asset.key);
+											setCurrentWorkout(
+												produce(currentWorkout, (draft) => {
+													const idx = draft.supersets.findIndex((s) =>
+														s.exercises.includes(exercise.identifier),
+													);
+													if (idx !== -1) {
+														if (draft.supersets[idx].exercises.length === 2)
+															draft.supersets.splice(idx, 1);
+														else
+															draft.supersets[idx].exercises = draft.supersets[
+																idx
+															].exercises.filter(
+																(e) => e !== exercise.identifier,
+															);
+													}
+													draft.exercises.splice(props.exerciseIdx, 1);
+												}),
+											);
+										},
+									);
 								}}
 							>
 								Remove
@@ -1545,25 +1552,25 @@ const ExerciseDisplay = (props: {
 																	const workout = await getWorkoutDetails(
 																		history.workoutId,
 																	);
-																	const yes = await confirmWrapper({
-																		confirmation: `Are you sure you want to copy all sets from "${workout.details.name}"?`,
-																	});
-																	if (yes) {
-																		const sets =
-																			workout.details.information.exercises[
-																				history.idx
-																			].sets;
-																		const converted = sets.map((set) =>
-																			convertHistorySetToCurrentSet(set),
-																		);
-																		setCurrentWorkout(
-																			produce(currentWorkout, (draft) => {
-																				draft.exercises[
-																					props.exerciseIdx
-																				].sets.push(...converted);
-																			}),
-																		);
-																	}
+																	openConfirmationModal(
+																		`Are you sure you want to copy all sets from "${workout.details.name}"?`,
+																		() => {
+																			const sets =
+																				workout.details.information.exercises[
+																					history.idx
+																				].sets;
+																			const converted = sets.map((set) =>
+																				convertHistorySetToCurrentSet(set),
+																			);
+																			setCurrentWorkout(
+																				produce(currentWorkout, (draft) => {
+																					draft.exercises[
+																						props.exerciseIdx
+																					].sets.push(...converted);
+																				}),
+																			);
+																		},
+																	);
 																}}
 															/>
 														) : null}
@@ -1985,25 +1992,20 @@ const SetDisplay = (props: {
 								color="red"
 								fz="xs"
 								leftSection={<IconTrash size={14} />}
-								onClick={async () => {
-									const yes = await match(!!set.confirmedAt)
-										.with(true, async () => {
-											return confirmWrapper({
-												confirmation:
-													"Are you sure you want to delete this set?",
-											});
-										})
-										.with(false, () => true)
-										.exhaustive();
-									if (yes)
-										setCurrentWorkout(
-											produce(currentWorkout, (draft) => {
-												draft.exercises[props.exerciseIdx].sets.splice(
-													props.setIdx,
-													1,
-												);
-											}),
-										);
+								onClick={() => {
+									openConfirmationModal(
+										"Are you sure you want to delete this set?",
+										() => {
+											setCurrentWorkout(
+												produce(currentWorkout, (draft) => {
+													draft.exercises[props.exerciseIdx].sets.splice(
+														props.setIdx,
+														1,
+													);
+												}),
+											);
+										},
+									);
 								}}
 							>
 								Delete set
@@ -2611,20 +2613,21 @@ const NoteInput = (props: {
 			/>
 			<ActionIcon
 				color="red"
-				onClick={async () => {
-					const yes = await confirmWrapper({
-						confirmation:
-							"This note will be deleted. Are you sure you want to continue?",
-					});
-					if (yes && currentWorkout)
-						setCurrentWorkout(
-							produce(currentWorkout, (draft) => {
-								draft.exercises[props.exerciseIdx].notes.splice(
-									props.noteIdx,
-									1,
+				onClick={() => {
+					openConfirmationModal(
+						"This note will be deleted. Are you sure you want to continue?",
+						() => {
+							if (currentWorkout)
+								setCurrentWorkout(
+									produce(currentWorkout, (draft) => {
+										draft.exercises[props.exerciseIdx].notes.splice(
+											props.noteIdx,
+											1,
+										);
+									}),
 								);
-							}),
-						);
+						},
+					);
 				}}
 			>
 				<IconTrash size={20} />
diff --git a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
index 7fd808a3eb..579a070c1f 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.$entity.$id._index.tsx
@@ -59,7 +59,6 @@ import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import { DisplayCollection } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	ExerciseHistory,
 	displayDistanceWithUnit,
@@ -70,6 +69,7 @@ import {
 	FitnessEntity,
 	PRO_REQUIRED_MESSAGE,
 	dayjsLib,
+	openConfirmationModal,
 } from "~/lib/generals";
 import {
 	useConfirmSubmit,
@@ -418,13 +418,13 @@ export default function Page() {
 										value={loaderData.entity}
 									/>
 									<Menu.Item
-										onClick={async (e) => {
+										onClick={(e) => {
 											const form = e.currentTarget.form;
 											e.preventDefault();
-											const conf = await confirmWrapper({
-												confirmation: `Are you sure you want to delete this ${loaderData.entity}? This action is not reversible.`,
-											});
-											if (conf && form) submit(form);
+											openConfirmationModal(
+												`Are you sure you want to delete this ${loaderData.entity}? This action is not reversible.`,
+												() => submit(form),
+											);
 										}}
 										color="red"
 										leftSection={<IconTrash size={14} />}
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
index a9d9776e18..cb91227831 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.item.$id._index.tsx
@@ -25,12 +25,14 @@ import {
 	rem,
 } from "@mantine/core";
 import { useDisclosure } from "@mantine/hooks";
-import type {
-	ActionFunctionArgs,
-	LoaderFunctionArgs,
-	MetaArgs,
-} from "@remix-run/node";
-import { Form, Link, useLoaderData, useNavigate } from "@remix-run/react";
+import { notifications } from "@mantine/notifications";
+import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node";
+import {
+	Link,
+	useLoaderData,
+	useNavigate,
+	useRevalidator,
+} from "@remix-run/react";
 import {
 	EntityLot,
 	ExerciseDetailsDocument,
@@ -58,8 +60,9 @@ import {
 	IconTrophy,
 	IconUser,
 } from "@tabler/icons-react";
-import { useQuery } from "@tanstack/react-query";
-import { Fragment } from "react";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { produce } from "immer";
+import { Fragment, useState } from "react";
 import { Virtuoso } from "react-virtuoso";
 import { $path } from "remix-routes";
 import invariant from "tiny-invariant";
@@ -78,11 +81,11 @@ import { MediaScrollArea } from "~/components/media";
 import {
 	FitnessEntity,
 	TimeSpan,
+	clientGqlService,
 	dayjsLib,
 	getDateFromTimeSpan,
 } from "~/lib/generals";
 import {
-	useComplexJsonUpdate,
 	useCoreDetails,
 	useIsFitnessActionActive,
 	useUserDetails,
@@ -96,7 +99,7 @@ import {
 	useMergingExercise,
 } from "~/lib/state/fitness";
 import { useAddEntityToCollection, useReviewEntity } from "~/lib/state/media";
-import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
+import { serverGqlService } from "~/lib/utilities.server";
 
 const searchParamsSchema = z.object({
 	defaultTab: z.string().optional(),
@@ -124,37 +127,13 @@ export const meta = ({ data }: MetaArgs<typeof loader>) => {
 	return [{ title: `${data?.exerciseDetails.name} | Ryot` }];
 };
 
-export const action = async ({ params, request }: ActionFunctionArgs) => {
-	const { id: exerciseId } = zx.parseParams(params, paramsSchema);
-	const entries = Object.entries(Object.fromEntries(await request.formData()));
-	const submission = [];
-	for (const [property, value] of entries) {
-		submission.push({
-			property,
-			value: value.toString(),
-		});
-	}
-	for (const change of submission) {
-		await serverGqlService.authenticatedRequest(
-			request,
-			UpdateUserExerciseSettingsDocument,
-			{ input: { change, exerciseId } },
-		);
-	}
-	return Response.json({ status: "success" } as const, {
-		headers: await createToastHeaders({
-			type: "success",
-			message: "Preferences updated",
-		}),
-	});
-};
-
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const coreDetails = useCoreDetails();
 	const userPreferences = useUserPreferences();
 	const unitSystem = useUserUnitSystem();
 	const userDetails = useUserDetails();
+	const revalidator = useRevalidator();
 	const canCurrentUserUpdate =
 		loaderData.exerciseDetails.source === ExerciseSource.Custom &&
 		userDetails.id === loaderData.exerciseDetails.createdByUserId;
@@ -174,7 +153,23 @@ export default function Page() {
 		updatePreferencesModalOpened,
 		{ open: openUpdatePreferencesModal, close: closeUpdatePreferencesModal },
 	] = useDisclosure(false);
-	const { toUpdatePreferences, appendPref } = useComplexJsonUpdate();
+	const [changingExerciseSettings, setChangingExerciseSettings] = useState({
+		isChanged: false,
+		value: loaderData.userExerciseDetails.details?.exerciseExtraInformation
+			?.settings || { excludeFromAnalytics: false, setRestTimers: {} },
+	});
+
+	const updateUserExerciseSettingsMutation = useMutation({
+		mutationFn: async () => {
+			await clientGqlService.request(UpdateUserExerciseSettingsDocument, {
+				input: {
+					exerciseId: loaderData.exerciseId,
+					change: changingExerciseSettings.value,
+				},
+			});
+		},
+	});
+
 	const computedDateAfterForCharts = getDateFromTimeSpan(timeSpanForCharts);
 	const filteredHistoryForCharts = sortBy(
 		loaderData.userExerciseDetails.history || [],
@@ -193,71 +188,78 @@ export default function Page() {
 	return (
 		<>
 			<Modal
+				centered
+				withCloseButton={false}
 				opened={updatePreferencesModalOpened}
 				onClose={() => closeUpdatePreferencesModal()}
-				withCloseButton={false}
-				centered
 			>
-				<Form method="POST" onSubmit={() => closeUpdatePreferencesModal()}>
-					<Stack>
-						{toUpdatePreferences.map((pref) => (
-							<input
-								hidden
-								readOnly
-								key={pref[0]}
-								name={pref[0]}
-								value={pref[1]}
-							/>
-						))}
-						<Switch
-							label="Exclude from analytics"
-							defaultChecked={
-								loaderData.userExerciseDetails.details?.exerciseExtraInformation
-									?.settings.excludeFromAnalytics
-							}
-							onChange={(ev) => {
-								appendPref(
-									"exclude_from_analytics",
-									String(ev.currentTarget.checked),
-								);
-							}}
-						/>
-						<Text size="sm">
-							When a new set is added, rest timers will be added automatically
-							according to the settings below.
-							<Text size="xs" c="dimmed" span>
-								{" "}
-								Default rest timer durations for all exercises can be changed in
-								the fitness preferences.
-							</Text>
+				<Stack>
+					<Switch
+						label="Exclude from analytics"
+						defaultChecked={
+							loaderData.userExerciseDetails.details?.exerciseExtraInformation
+								?.settings.excludeFromAnalytics
+						}
+						onChange={(ev) => {
+							setChangingExerciseSettings(
+								produce(changingExerciseSettings, (draft) => {
+									draft.isChanged = true;
+									draft.value.excludeFromAnalytics = ev.currentTarget.checked;
+								}),
+							);
+						}}
+					/>
+					<Text size="sm">
+						When a new set is added, rest timers will be added automatically
+						according to the settings below.
+						<Text size="xs" c="dimmed" span>
+							{" "}
+							Default rest timer durations for all exercises can be changed in
+							the fitness preferences.
 						</Text>
-						<SimpleGrid cols={2}>
-							{(["normal", "warmup", "drop", "failure"] as const).map(
-								(name) => {
-									const value =
-										loaderData.userExerciseDetails.details
-											?.exerciseExtraInformation?.settings.setRestTimers[name];
-									return (
-										<NumberInput
-											suffix="s"
-											key={name}
-											label={changeCase(snakeCase(name))}
-											defaultValue={isNumber(value) ? value : undefined}
-											onChange={(val) => {
-												if (isNumber(val))
-													appendPref(
-														`set_rest_timers.${snakeCase(name)}`,
-														String(val),
-													);
-											}}
-										/>
-									);
-								},
-							)}
-						</SimpleGrid>
-						<Button type="submit">Save settings for exercise</Button>
-					</Stack>
-				</Form>
+					</Text>
+					<SimpleGrid cols={2}>
+						{(["normal", "warmup", "drop", "failure"] as const).map((name) => {
+							const value =
+								loaderData.userExerciseDetails.details?.exerciseExtraInformation
+									?.settings.setRestTimers[name];
+							return (
+								<NumberInput
+									suffix="s"
+									key={name}
+									label={changeCase(snakeCase(name))}
+									defaultValue={isNumber(value) ? value : undefined}
+									onChange={(val) => {
+										if (isNumber(val))
+											setChangingExerciseSettings(
+												produce(changingExerciseSettings, (draft) => {
+													draft.isChanged = true;
+													draft.value.setRestTimers[name] = val;
+												}),
+											);
+									}}
+								/>
+							);
+						})}
+					</SimpleGrid>
+					<Button
+						type="submit"
+						disabled={!changingExerciseSettings.isChanged}
+						loading={updateUserExerciseSettingsMutation.isPending}
+						onClick={async () => {
+							await updateUserExerciseSettingsMutation.mutateAsync();
+							revalidator.revalidate();
+							notifications.show({
+								color: "green",
+								title: "Settings updated",
+								message: "Settings for the exercise have been updated.",
+							});
+							closeUpdatePreferencesModal();
+						}}
+					>
+						Save settings
+					</Button>
+				</Stack>
 			</Modal>
 			<Container size="xs" px="lg">
 				<Stack>
diff --git a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
index 51c42f9608..1be430f64c 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.exercises.list.tsx
@@ -58,10 +58,10 @@ import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import { DebouncedSearchInput, FiltersModal } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	dayjsLib,
 	getExerciseDetailsPath,
+	openConfirmationModal,
 	pageQueryParam,
 } from "~/lib/generals";
 import {
@@ -325,26 +325,25 @@ export default function Page() {
 											<Link
 												style={{ all: "unset", cursor: "pointer" }}
 												to={getExerciseDetailsPath(exercise.id)}
-												onClick={async (e) => {
+												onClick={(e) => {
 													if (allowAddingExerciseToWorkout) return;
 													if (mergingExercise) {
 														e.preventDefault();
-														const conf = await confirmWrapper({
-															confirmation:
-																"Are you sure you want to merge this exercise? This will replace this exercise in all workouts.",
-														});
-														if (conf) {
-															const formData = new FormData();
-															formData.append("mergeFrom", mergingExercise);
-															formData.append("mergeInto", exercise.id);
-															setMergingExercise(null);
-															submit(formData, {
-																method: "POST",
-																action: withQuery(".", {
-																	intent: "mergeExercise",
-																}),
-															});
-														}
+														openConfirmationModal(
+															"Are you sure you want to merge this exercise? This will replace this exercise in all workouts.",
+															() => {
+																const formData = new FormData();
+																formData.append("mergeFrom", mergingExercise);
+																formData.append("mergeInto", exercise.id);
+																setMergingExercise(null);
+																submit(formData, {
+																	method: "POST",
+																	action: withQuery(".", {
+																		intent: "mergeExercise",
+																	}),
+																});
+															},
+														);
 														return;
 													}
 													if (currentWorkout) {
diff --git a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
index defa931fb5..5a86603540 100644
--- a/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
+++ b/apps/frontend/app/routes/_dashboard.fitness.measurements.list.tsx
@@ -35,13 +35,13 @@ import { withQuery } from "ufo";
 import { useLocalStorage } from "usehooks-ts";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	TimeSpan,
 	dayjsLib,
 	generateColor,
 	getDateFromTimeSpan,
 	getStringAsciiValue,
+	openConfirmationModal,
 } from "~/lib/generals";
 import {
 	useAppSearchParam,
@@ -265,14 +265,13 @@ export default function Page() {
 											<ActionIcon
 												color="red"
 												type="submit"
-												onClick={async (e) => {
+												onClick={(e) => {
 													const form = e.currentTarget.form;
 													e.preventDefault();
-													const conf = await confirmWrapper({
-														confirmation:
-															"This action can not be undone. Are you sure you want to delete this measurement?",
-													});
-													if (conf && form) submit(form);
+													openConfirmationModal(
+														"This action can not be undone. Are you sure you want to delete this measurement?",
+														() => submit(form),
+													);
 												}}
 											>
 												<IconTrash />
diff --git a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
index 9f99ea829c..eff43b6750 100644
--- a/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.$action.$lot.tsx
@@ -271,7 +271,11 @@ export default function Page() {
 							component={Link}
 							variant="transparent"
 							leftSection={<IconPhotoPlus />}
-							to={$path("/media/create", { lot: loaderData.lot })}
+							to={$path(
+								"/media/update/:action",
+								{ action: "create" },
+								{ lot: loaderData.lot },
+							)}
 						>
 							Create
 						</Button>
diff --git a/apps/frontend/app/routes/_dashboard.media.create.tsx b/apps/frontend/app/routes/_dashboard.media.create.tsx
deleted file mode 100644
index 0a55245a9f..0000000000
--- a/apps/frontend/app/routes/_dashboard.media.create.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-import {
-	Anchor,
-	Button,
-	Code,
-	Container,
-	FileInput,
-	Group,
-	JsonInput,
-	NumberInput,
-	Select,
-	Stack,
-	Switch,
-	TextInput,
-	Textarea,
-	Title,
-} from "@mantine/core";
-import {
-	type ActionFunctionArgs,
-	type LoaderFunctionArgs,
-	type MetaArgs,
-	redirect,
-	unstable_parseMultipartFormData,
-} from "@remix-run/node";
-import { Form, useLoaderData } from "@remix-run/react";
-import {
-	CreateCustomMetadataDocument,
-	MediaLot,
-} from "@ryot/generated/graphql/backend/graphql";
-import { camelCase, changeCase, processSubmission } from "@ryot/ts-utils";
-import { IconCalendar, IconPhoto, IconVideo } from "@tabler/icons-react";
-import { $path } from "remix-routes";
-import { z } from "zod";
-import { zx } from "zodix";
-import { useCoreDetails } from "~/lib/hooks";
-import { s3FileUploader, serverGqlService } from "~/lib/utilities.server";
-
-const searchParamsSchema = z.object({
-	lot: z.nativeEnum(MediaLot).optional(),
-});
-
-export type SearchParams = z.infer<typeof searchParamsSchema>;
-
-export const loader = async ({ request }: LoaderFunctionArgs) => {
-	const query = zx.parseQuery(request, searchParamsSchema);
-	return { query };
-};
-
-export const meta = (_args: MetaArgs<typeof loader>) => {
-	return [{ title: "Create Media | Ryot" }];
-};
-
-export const action = async ({ request }: ActionFunctionArgs) => {
-	const uploaders = s3FileUploader("metadata");
-	const formData = await unstable_parseMultipartFormData(request, uploaders);
-	const submission = processSubmission(formData, schema);
-	// biome-ignore lint/suspicious/noExplicitAny: required here
-	const input: any = {
-		...submission,
-		[`${camelCase(submission.lot)}Specifics`]: submission.specifics
-			? JSON.parse(submission.specifics)
-			: undefined,
-	};
-	input.specifics = undefined;
-	input.genres = input.genres?.split(",");
-	input.creators = input.creators?.split(",");
-	const { createCustomMetadata } = await serverGqlService.authenticatedRequest(
-		request,
-		CreateCustomMetadataDocument,
-		{ input },
-	);
-	return redirect($path("/media/item/:id", { id: createCustomMetadata.id }));
-};
-
-const optionalString = z.string().optional();
-const optionalStringArray = z.array(z.string()).optional();
-
-const schema = z.object({
-	title: z.string(),
-	lot: z.nativeEnum(MediaLot),
-	images: optionalStringArray,
-	videos: optionalStringArray,
-	description: optionalString,
-	isNsfw: z.boolean().optional(),
-	publishYear: z.number().optional(),
-	genres: optionalString,
-	creators: optionalString,
-	specifics: optionalString,
-});
-
-export default function Page() {
-	const loaderData = useLoaderData<typeof loader>();
-	const coreDetails = useCoreDetails();
-	const fileUploadNotAllowed = !coreDetails.fileStorageEnabled;
-
-	return (
-		<Container>
-			<Form method="POST" encType="multipart/form-data">
-				<Stack>
-					<Title>Create Media</Title>
-					<TextInput label="Title" required autoFocus name="title" />
-					<Group wrap="nowrap">
-						<Select
-							required
-							name="lot"
-							label="Type"
-							defaultValue={loaderData.query.lot}
-							data={Object.values(MediaLot).map((v) => ({
-								value: v,
-								label: changeCase(v),
-							}))}
-						/>
-						<Switch mt="md" label="Is it NSFW?" name="isNsfw" />
-					</Group>
-					<JsonInput
-						label="Specifics"
-						formatOnBlur
-						name="specifics"
-						description={
-							<>
-								Please search for <Code>Specifics</Code> inputs at the{" "}
-								<Anchor href="/backend/graphql" size="xs" target="_blank">
-									graphql endpoint
-								</Anchor>{" "}
-								for the required JSON structure
-							</>
-						}
-					/>
-					<Textarea
-						label="Description"
-						description="Markdown is supported"
-						name="description"
-					/>
-					<FileInput
-						label="Images"
-						name="images"
-						multiple
-						disabled={fileUploadNotAllowed}
-						description={
-							fileUploadNotAllowed &&
-							"Please set the S3 variables required to enable file uploading"
-						}
-						accept="image/png,image/jpeg,image/jpg"
-						leftSection={<IconPhoto />}
-					/>
-					<FileInput
-						label="Videos"
-						name="videos"
-						multiple
-						disabled={fileUploadNotAllowed}
-						description={
-							fileUploadNotAllowed &&
-							"Please set the S3 variables required to enable file uploading"
-						}
-						accept="video/mp4,video/x-m4v,video/*"
-						leftSection={<IconVideo />}
-					/>
-					<NumberInput
-						label="Publish year"
-						leftSection={<IconCalendar />}
-						name="publishYear"
-					/>
-					<TextInput
-						label="Creators"
-						placeholder="Comma separated names"
-						name="creators"
-					/>
-					<TextInput
-						label="Genres"
-						placeholder="Comma separated values"
-						name="genres"
-					/>
-					<Button type="submit">Create</Button>
-				</Stack>
-			</Form>
-		</Container>
-	);
-}
diff --git a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
index 290ebb358d..cab4412e61 100644
--- a/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.media.item.$id._index.tsx
@@ -1,6 +1,5 @@
 import { useAutoAnimate } from "@formkit/auto-animate/react";
 import {
-	Accordion,
 	ActionIcon,
 	Alert,
 	Anchor,
@@ -104,7 +103,6 @@ import {
 	MediaDetailsLayout,
 	ReviewItemDisplay,
 } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	MediaScrollArea,
 	PartialMetadataDisplay,
@@ -115,6 +113,7 @@ import {
 	Verb,
 	dayjsLib,
 	getVerb,
+	openConfirmationModal,
 	refreshUserMetadataDetails,
 	reviewYellow,
 } from "~/lib/generals";
@@ -254,7 +253,11 @@ export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const userPreferences = useUserPreferences();
 	const events = useApplicationEvents();
+	const userDetails = useUserDetails();
 	const submit = useConfirmSubmit();
+	const canCurrentUserUpdate =
+		loaderData.metadataDetails.source === MediaSource.Custom &&
+		userDetails.id === loaderData.metadataDetails.createdByUserId;
 	const [tab, setTab] = useState<string | null>(
 		loaderData.query.defaultTab || "overview",
 	);
@@ -267,6 +270,8 @@ export default function Page() {
 	const [_m, setMetadataToUpdate] = useMetadataProgressUpdate();
 	const [_r, setEntityToReview] = useReviewEntity();
 	const [_a, setAddEntityToCollectionData] = useAddEntityToCollection();
+	const [openedShowSeason, setOpenedShowSeason] = useState<number>();
+
 	const inProgress = loaderData.userMetadataDetails.inProgress;
 	const nextEntry = loaderData.userMetadataDetails.nextEntry;
 
@@ -318,883 +323,877 @@ export default function Page() {
 	};
 
 	return (
-		<Container>
-			<MediaDetailsLayout
-				images={loaderData.metadataDetails.assets.images}
-				externalLink={{
-					source: loaderData.metadataDetails.source,
-					lot: loaderData.metadataDetails.lot,
-					href: loaderData.metadataDetails.sourceUrl,
-				}}
-			>
-				<Box>
-					{userPreferences.featuresEnabled.media.groups &&
-					loaderData.metadataDetails.group ? (
-						<Link
-							to={$path("/media/groups/item/:id", {
-								id: loaderData.metadataDetails.group.id,
-							})}
-							style={{ color: "unset" }}
-						>
-							<Text c="dimmed" fs="italic">
-								{loaderData.metadataDetails.group.name} #
-								{loaderData.metadataDetails.group.part}
-							</Text>
-						</Link>
-					) : null}
-					<Title id="media-title">{loaderData.metadataDetails.title}</Title>
-				</Box>
-				{loaderData.userMetadataDetails.collections.length > 0 ? (
-					<Group>
-						{loaderData.userMetadataDetails.collections.map((col) => (
-							<DisplayCollection
-								col={col}
-								key={col.id}
-								creatorUserId={col.userId}
-								entityLot={EntityLot.Metadata}
-								entityId={loaderData.metadataId}
-							/>
-						))}
-					</Group>
-				) : null}
-				<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
-					{[
-						loaderData.metadataDetails.publishDate
-							? dayjsLib(loaderData.metadataDetails.publishDate).format("LL")
-							: loaderData.metadataDetails.publishYear,
-						loaderData.metadataDetails.originalLanguage,
-						loaderData.metadataDetails.productionStatus,
-						loaderData.metadataDetails.bookSpecifics?.pages &&
-							`${loaderData.metadataDetails.bookSpecifics.pages} pages`,
-						loaderData.metadataDetails.podcastSpecifics?.totalEpisodes &&
-							`${loaderData.metadataDetails.podcastSpecifics.totalEpisodes} episodes`,
-						loaderData.metadataDetails.animeSpecifics?.episodes &&
-							`${loaderData.metadataDetails.animeSpecifics.episodes} episodes`,
-						loaderData.metadataDetails.mangaSpecifics?.chapters &&
-							`${loaderData.metadataDetails.mangaSpecifics.chapters} chapters`,
-						loaderData.metadataDetails.mangaSpecifics?.volumes &&
-							`${loaderData.metadataDetails.mangaSpecifics.volumes} volumes`,
-						loaderData.metadataDetails.movieSpecifics?.runtime &&
-							humanizeDuration(
-								dayjsLib
-									.duration(
-										loaderData.metadataDetails.movieSpecifics.runtime,
-										"minute",
-									)
-									.asMilliseconds(),
-							),
-						loaderData.metadataDetails.showSpecifics?.totalSeasons &&
-							`${loaderData.metadataDetails.showSpecifics.totalSeasons} seasons`,
-						loaderData.metadataDetails.showSpecifics?.totalEpisodes &&
-							`${loaderData.metadataDetails.showSpecifics.totalEpisodes} episodes`,
-						loaderData.metadataDetails.showSpecifics?.runtime &&
-							humanizeDuration(
-								dayjsLib
-									.duration(
-										loaderData.metadataDetails.showSpecifics.runtime,
-										"minute",
-									)
-									.asMilliseconds(),
-							),
-						loaderData.metadataDetails.audioBookSpecifics?.runtime &&
-							humanizeDuration(
-								dayjsLib
-									.duration(
-										loaderData.metadataDetails.audioBookSpecifics.runtime,
-										"minute",
-									)
-									.asMilliseconds(),
-							),
-						loaderData.metadataDetails.musicSpecifics?.duration &&
-							humanizeDuration(
-								dayjsLib
-									.duration(
-										loaderData.metadataDetails.musicSpecifics.duration,
-										"second",
-									)
-									.asMilliseconds(),
-							),
-						loaderData.metadataDetails.musicSpecifics?.viewCount &&
-							formatQuantityWithCompactNotation(
-								loaderData.metadataDetails.musicSpecifics.viewCount,
-							),
-						loaderData.metadataDetails.musicSpecifics?.byVariousArtists &&
-							"Various Artists",
-					]
-						.filter(Boolean)
-						.join(" • ")}
-				</Text>
-				{loaderData.metadataDetails.providerRating ||
-				loaderData.userMetadataDetails.averageRating ? (
-					<Group>
-						{loaderData.metadataDetails.providerRating ? (
-							<Paper
-								p={4}
-								display="flex"
-								style={{
-									flexDirection: "column",
-									alignItems: "center",
-									gap: 6,
-								}}
+		<>
+			<DisplayShowSeasonEpisodesModal
+				openedShowSeason={openedShowSeason}
+				setOpenedShowSeason={setOpenedShowSeason}
+			/>
+			<Container>
+				<MediaDetailsLayout
+					images={loaderData.metadataDetails.assets.images}
+					externalLink={{
+						source: loaderData.metadataDetails.source,
+						lot: loaderData.metadataDetails.lot,
+						href: loaderData.metadataDetails.sourceUrl,
+					}}
+				>
+					<Box>
+						{userPreferences.featuresEnabled.media.groups &&
+						loaderData.metadataDetails.group ? (
+							<Link
+								to={$path("/media/groups/item/:id", {
+									id: loaderData.metadataDetails.group.id,
+								})}
+								style={{ color: "unset" }}
 							>
-								<Image
-									alt="Logo"
-									h={24}
-									w={24}
-									src={`/provider-logos/${match(
-										loaderData.metadataDetails.source,
-									)
-										.with(MediaSource.Anilist, () => "anilist.svg")
-										.with(MediaSource.Audible, () => "audible.svg")
-										.with(MediaSource.GoogleBooks, () => "google-books.svg")
-										.with(MediaSource.Igdb, () => "igdb.svg")
-										.with(MediaSource.Itunes, () => "itunes.svg")
-										.with(MediaSource.Listennotes, () => "listennotes.webp")
-										.with(MediaSource.Mal, () => "mal.svg")
-										.with(MediaSource.MangaUpdates, () => "manga-updates.svg")
-										.with(MediaSource.Openlibrary, () => "openlibrary.svg")
-										.with(MediaSource.Tmdb, () => "tmdb.svg")
-										.with(MediaSource.Vndb, () => "vndb.ico")
-										.with(MediaSource.YoutubeMusic, () => "youtube-music.png")
-										.with(MediaSource.Custom, () => undefined)
-										.exhaustive()}`}
+								<Text c="dimmed" fs="italic">
+									{loaderData.metadataDetails.group.name} #
+									{loaderData.metadataDetails.group.part}
+								</Text>
+							</Link>
+						) : null}
+						<Title id="media-title">{loaderData.metadataDetails.title}</Title>
+					</Box>
+					{loaderData.userMetadataDetails.collections.length > 0 ? (
+						<Group>
+							{loaderData.userMetadataDetails.collections.map((col) => (
+								<DisplayCollection
+									col={col}
+									key={col.id}
+									creatorUserId={col.userId}
+									entityLot={EntityLot.Metadata}
+									entityId={loaderData.metadataId}
 								/>
-								<Text fz="sm">
-									{Number(loaderData.metadataDetails.providerRating).toFixed(1)}
-									{match(loaderData.metadataDetails.source)
-										.with(
-											MediaSource.Anilist,
-											MediaSource.Igdb,
-											MediaSource.Listennotes,
-											MediaSource.Tmdb,
-											MediaSource.Vndb,
-											() => "%",
+							))}
+						</Group>
+					) : null}
+					<Text c="dimmed" fz={{ base: "sm", lg: "md" }}>
+						{[
+							loaderData.metadataDetails.publishDate
+								? dayjsLib(loaderData.metadataDetails.publishDate).format("LL")
+								: loaderData.metadataDetails.publishYear,
+							loaderData.metadataDetails.originalLanguage,
+							loaderData.metadataDetails.productionStatus,
+							loaderData.metadataDetails.bookSpecifics?.pages &&
+								`${loaderData.metadataDetails.bookSpecifics.pages} pages`,
+							loaderData.metadataDetails.podcastSpecifics?.totalEpisodes &&
+								`${loaderData.metadataDetails.podcastSpecifics.totalEpisodes} episodes`,
+							loaderData.metadataDetails.animeSpecifics?.episodes &&
+								`${loaderData.metadataDetails.animeSpecifics.episodes} episodes`,
+							loaderData.metadataDetails.mangaSpecifics?.chapters &&
+								`${loaderData.metadataDetails.mangaSpecifics.chapters} chapters`,
+							loaderData.metadataDetails.mangaSpecifics?.volumes &&
+								`${loaderData.metadataDetails.mangaSpecifics.volumes} volumes`,
+							loaderData.metadataDetails.movieSpecifics?.runtime &&
+								humanizeDuration(
+									dayjsLib
+										.duration(
+											loaderData.metadataDetails.movieSpecifics.runtime,
+											"minute",
 										)
-										.with(
-											MediaSource.Audible,
-											MediaSource.GoogleBooks,
-											() => "/5",
+										.asMilliseconds(),
+								),
+							loaderData.metadataDetails.showSpecifics?.totalSeasons &&
+								`${loaderData.metadataDetails.showSpecifics.totalSeasons} seasons`,
+							loaderData.metadataDetails.showSpecifics?.totalEpisodes &&
+								`${loaderData.metadataDetails.showSpecifics.totalEpisodes} episodes`,
+							loaderData.metadataDetails.showSpecifics?.runtime &&
+								humanizeDuration(
+									dayjsLib
+										.duration(
+											loaderData.metadataDetails.showSpecifics.runtime,
+											"minute",
 										)
-										.with(
-											MediaSource.Mal,
-											MediaSource.MangaUpdates,
-											() => "/10",
+										.asMilliseconds(),
+								),
+							loaderData.metadataDetails.audioBookSpecifics?.runtime &&
+								humanizeDuration(
+									dayjsLib
+										.duration(
+											loaderData.metadataDetails.audioBookSpecifics.runtime,
+											"minute",
 										)
-										.with(
-											MediaSource.Custom,
-											MediaSource.Itunes,
-											MediaSource.Openlibrary,
-											MediaSource.YoutubeMusic,
-											() => undefined,
+										.asMilliseconds(),
+								),
+							loaderData.metadataDetails.musicSpecifics?.duration &&
+								humanizeDuration(
+									dayjsLib
+										.duration(
+											loaderData.metadataDetails.musicSpecifics.duration,
+											"second",
 										)
-										.exhaustive()}
-								</Text>
-							</Paper>
-						) : null}
-						{loaderData.userMetadataDetails.averageRating
-							? match(userPreferences.general.reviewScale)
-									.with(UserReviewScale.ThreePointSmiley, () => (
-										<DisplayThreePointReview
-											rating={loaderData.userMetadataDetails.averageRating}
-											size={40}
-										/>
-									))
-									.otherwise(() => (
-										<Paper
-											p={4}
-											display="flex"
-											style={{
-												flexDirection: "column",
-												alignItems: "center",
-												gap: 6,
-											}}
-										>
-											<IconStarFilled
-												size={22}
-												style={{ color: reviewYellow }}
+										.asMilliseconds(),
+								),
+							loaderData.metadataDetails.musicSpecifics?.viewCount &&
+								formatQuantityWithCompactNotation(
+									loaderData.metadataDetails.musicSpecifics.viewCount,
+								),
+							loaderData.metadataDetails.musicSpecifics?.byVariousArtists &&
+								"Various Artists",
+						]
+							.filter(Boolean)
+							.join(" • ")}
+					</Text>
+					{loaderData.metadataDetails.providerRating ||
+					loaderData.userMetadataDetails.averageRating ? (
+						<Group>
+							{loaderData.metadataDetails.providerRating ? (
+								<Paper
+									p={4}
+									display="flex"
+									style={{
+										flexDirection: "column",
+										alignItems: "center",
+										gap: 6,
+									}}
+								>
+									<Image
+										alt="Logo"
+										h={24}
+										w={24}
+										src={`/provider-logos/${match(
+											loaderData.metadataDetails.source,
+										)
+											.with(MediaSource.Anilist, () => "anilist.svg")
+											.with(MediaSource.Audible, () => "audible.svg")
+											.with(MediaSource.GoogleBooks, () => "google-books.svg")
+											.with(MediaSource.Igdb, () => "igdb.svg")
+											.with(MediaSource.Itunes, () => "itunes.svg")
+											.with(MediaSource.Listennotes, () => "listennotes.webp")
+											.with(MediaSource.Mal, () => "mal.svg")
+											.with(MediaSource.MangaUpdates, () => "manga-updates.svg")
+											.with(MediaSource.Openlibrary, () => "openlibrary.svg")
+											.with(MediaSource.Tmdb, () => "tmdb.svg")
+											.with(MediaSource.Vndb, () => "vndb.ico")
+											.with(MediaSource.YoutubeMusic, () => "youtube-music.png")
+											.with(MediaSource.Custom, () => undefined)
+											.exhaustive()}`}
+									/>
+									<Text fz="sm">
+										{Number(loaderData.metadataDetails.providerRating).toFixed(
+											1,
+										)}
+										{match(loaderData.metadataDetails.source)
+											.with(
+												MediaSource.Anilist,
+												MediaSource.Igdb,
+												MediaSource.Listennotes,
+												MediaSource.Tmdb,
+												MediaSource.Vndb,
+												() => "%",
+											)
+											.with(
+												MediaSource.Audible,
+												MediaSource.GoogleBooks,
+												() => "/5",
+											)
+											.with(
+												MediaSource.Mal,
+												MediaSource.MangaUpdates,
+												() => "/10",
+											)
+											.with(
+												MediaSource.Custom,
+												MediaSource.Itunes,
+												MediaSource.Openlibrary,
+												MediaSource.YoutubeMusic,
+												() => undefined,
+											)
+											.exhaustive()}
+									</Text>
+								</Paper>
+							) : null}
+							{loaderData.userMetadataDetails.averageRating
+								? match(userPreferences.general.reviewScale)
+										.with(UserReviewScale.ThreePointSmiley, () => (
+											<DisplayThreePointReview
+												rating={loaderData.userMetadataDetails.averageRating}
+												size={40}
 											/>
-											<Text fz="sm">
-												{Number(
-													loaderData.userMetadataDetails.averageRating,
-												).toFixed(1)}
-												{userPreferences.general.reviewScale ===
-												UserReviewScale.OutOfFive
-													? undefined
-													: "%"}
-											</Text>
-										</Paper>
-									))
-							: null}
-					</Group>
-				) : null}
-				{inProgress ? (
-					<Alert icon={<IconAlertCircle />} variant="outline">
-						You are currently{" "}
-						{getVerb(Verb.Read, loaderData.metadataDetails.lot)}
-						ing{" "}
-						{inProgress.podcastExtraInformation
-							? `EP-${inProgress.podcastExtraInformation.episode}`
-							: inProgress.showExtraInformation
-								? `S${inProgress.showExtraInformation.season}-E${inProgress.showExtraInformation.episode}`
-								: "this"}{" "}
-						({Number(inProgress.progress).toFixed(2)}
-						%)
-					</Alert>
-				) : null}
-				<Tabs variant="outline" value={tab} onChange={(t) => setTab(t)}>
-					<Tabs.List mb="xs">
-						<Tabs.Tab
-							value="overview"
-							leftSection={<IconInfoCircle size={16} />}
-						>
-							Overview
-						</Tabs.Tab>
-						<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
-							Actions
-						</Tabs.Tab>
-						<Tabs.Tab
-							value="history"
-							leftSection={<IconRotateClockwise size={16} />}
-						>
-							History
-						</Tabs.Tab>
-						{loaderData.metadataDetails.lot === MediaLot.Show ? (
+										))
+										.otherwise(() => (
+											<Paper
+												p={4}
+												display="flex"
+												style={{
+													flexDirection: "column",
+													alignItems: "center",
+													gap: 6,
+												}}
+											>
+												<IconStarFilled
+													size={22}
+													style={{ color: reviewYellow }}
+												/>
+												<Text fz="sm">
+													{Number(
+														loaderData.userMetadataDetails.averageRating,
+													).toFixed(1)}
+													{userPreferences.general.reviewScale ===
+													UserReviewScale.OutOfFive
+														? undefined
+														: "%"}
+												</Text>
+											</Paper>
+										))
+								: null}
+						</Group>
+					) : null}
+					{inProgress ? (
+						<Alert icon={<IconAlertCircle />} variant="outline">
+							You are currently{" "}
+							{getVerb(Verb.Read, loaderData.metadataDetails.lot)}
+							ing{" "}
+							{inProgress.podcastExtraInformation
+								? `EP-${inProgress.podcastExtraInformation.episode}`
+								: inProgress.showExtraInformation
+									? `S${inProgress.showExtraInformation.season}-E${inProgress.showExtraInformation.episode}`
+									: "this"}{" "}
+							({Number(inProgress.progress).toFixed(2)}
+							%)
+						</Alert>
+					) : null}
+					<Tabs variant="outline" value={tab} onChange={(t) => setTab(t)}>
+						<Tabs.List mb="xs">
 							<Tabs.Tab
-								value="showSeasons"
-								leftSection={<IconPlayerPlay size={16} />}
+								value="overview"
+								leftSection={<IconInfoCircle size={16} />}
 							>
-								Seasons
+								Overview
 							</Tabs.Tab>
-						) : null}
-						{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
-							<Tabs.Tab
-								value="podcastEpisodes"
-								leftSection={<IconPlayerPlay size={16} />}
-							>
-								Episodes
+							<Tabs.Tab value="actions" leftSection={<IconUser size={16} />}>
+								Actions
 							</Tabs.Tab>
-						) : null}
-						{!userPreferences.general.disableReviews ? (
 							<Tabs.Tab
-								value="reviews"
-								leftSection={<IconMessageCircle2 size={16} />}
+								value="history"
+								leftSection={<IconRotateClockwise size={16} />}
 							>
-								Reviews
-							</Tabs.Tab>
-						) : null}
-						<Tabs.Tab value="suggestions" leftSection={<IconBulb size={16} />}>
-							Suggestions
-						</Tabs.Tab>
-						{!userPreferences.general.disableVideos &&
-						(loaderData.metadataDetails.assets.videos.length || 0) > 0 ? (
-							<Tabs.Tab value="videos" leftSection={<IconVideo size={16} />}>
-								Videos
+								History
 							</Tabs.Tab>
-						) : null}
-						{!userPreferences.general.disableWatchProviders ? (
+							{loaderData.metadataDetails.lot === MediaLot.Show ? (
+								<Tabs.Tab
+									value="showSeasons"
+									leftSection={<IconPlayerPlay size={16} />}
+								>
+									Seasons
+								</Tabs.Tab>
+							) : null}
+							{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
+								<Tabs.Tab
+									value="podcastEpisodes"
+									leftSection={<IconPlayerPlay size={16} />}
+								>
+									Episodes
+								</Tabs.Tab>
+							) : null}
+							{!userPreferences.general.disableReviews ? (
+								<Tabs.Tab
+									value="reviews"
+									leftSection={<IconMessageCircle2 size={16} />}
+								>
+									Reviews
+								</Tabs.Tab>
+							) : null}
 							<Tabs.Tab
-								value="watchProviders"
-								leftSection={<IconMovie size={16} />}
+								value="suggestions"
+								leftSection={<IconBulb size={16} />}
 							>
-								Watch On
+								Suggestions
 							</Tabs.Tab>
-						) : null}
-					</Tabs.List>
-					<Tabs.Panel value="overview">
-						<MediaScrollArea>
-							<Stack gap="sm">
-								<SimpleGrid
-									cols={{ base: 3, xl: 4 }}
-									spacing={{ base: "md", lg: "xs" }}
+							{!userPreferences.general.disableVideos &&
+							(loaderData.metadataDetails.assets.videos.length || 0) > 0 ? (
+								<Tabs.Tab value="videos" leftSection={<IconVideo size={16} />}>
+									Videos
+								</Tabs.Tab>
+							) : null}
+							{!userPreferences.general.disableWatchProviders ? (
+								<Tabs.Tab
+									value="watchProviders"
+									leftSection={<IconMovie size={16} />}
 								>
-									{userPreferences.featuresEnabled.media.genres
-										? loaderData.metadataDetails.genres
-												.slice(0, 12)
-												.map((g) => {
-													const color = useGetRandomMantineColor(g.name);
-													return (
-														<Group key={g.id} wrap="nowrap">
-															<Box
-																h={11}
-																w={11}
-																bg={color}
-																style={{ borderRadius: 2, flex: "none" }}
-															/>
-															<Anchor
-																component={Link}
-																to={$path("/media/genre/:id", {
-																	id: g.id,
-																})}
-																fz="sm"
-																truncate
-															>
-																{g.name.trim()}
-															</Anchor>
-														</Group>
-													);
-												})
-										: null}
-								</SimpleGrid>
-								{loaderData.metadataDetails.description ? (
-									<div
-										// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
-										dangerouslySetInnerHTML={{
-											__html: loaderData.metadataDetails.description,
-										}}
-									/>
-								) : null}
-								{userPreferences.featuresEnabled.media.people ? (
-									<Stack>
-										{loaderData.metadataDetails.creators.map((c) => (
-											<Box key={c.name}>
-												<Text fw="bold">{c.name}</Text>
-												<ScrollArea
-													mt="xs"
-													w={{
-														base: 380,
-														xs: 440,
-														sm: 480,
-														md: 520,
-														lg: 580,
-													}}
-												>
-													<Flex gap="md">
-														{c.items.map((creator) => (
-															<Box key={`${creator.id}-${creator.name}`}>
-																{creator.id ? (
-																	<Anchor
-																		component={Link}
-																		data-creator-id={creator.id}
-																		to={$path("/media/people/item/:id", {
-																			id: creator.id,
-																		})}
-																	>
+									Watch On
+								</Tabs.Tab>
+							) : null}
+						</Tabs.List>
+						<Tabs.Panel value="overview">
+							<MediaScrollArea>
+								<Stack gap="sm">
+									<SimpleGrid
+										cols={{ base: 3, xl: 4 }}
+										spacing={{ base: "md", lg: "xs" }}
+									>
+										{userPreferences.featuresEnabled.media.genres
+											? loaderData.metadataDetails.genres
+													.slice(0, 12)
+													.map((g) => {
+														const color = useGetRandomMantineColor(g.name);
+														return (
+															<Group key={g.id} wrap="nowrap">
+																<Box
+																	h={11}
+																	w={11}
+																	bg={color}
+																	style={{ borderRadius: 2, flex: "none" }}
+																/>
+																<Anchor
+																	component={Link}
+																	to={$path("/media/genre/:id", {
+																		id: g.id,
+																	})}
+																	fz="sm"
+																	truncate
+																>
+																	{g.name.trim()}
+																</Anchor>
+															</Group>
+														);
+													})
+											: null}
+									</SimpleGrid>
+									{loaderData.metadataDetails.description ? (
+										<div
+											// biome-ignore lint/security/noDangerouslySetInnerHtml: generated by the backend securely
+											dangerouslySetInnerHTML={{
+												__html: loaderData.metadataDetails.description,
+											}}
+										/>
+									) : null}
+									{userPreferences.featuresEnabled.media.people ? (
+										<Stack>
+											{loaderData.metadataDetails.creators.map((c) => (
+												<Box key={c.name}>
+													<Text fw="bold">{c.name}</Text>
+													<ScrollArea
+														mt="xs"
+														w={{
+															base: 380,
+															xs: 440,
+															sm: 480,
+															md: 520,
+															lg: 580,
+														}}
+													>
+														<Flex gap="md">
+															{c.items.map((creator) => (
+																<Box key={`${creator.id}-${creator.name}`}>
+																	{creator.id ? (
+																		<Anchor
+																			component={Link}
+																			data-creator-id={creator.id}
+																			to={$path("/media/people/item/:id", {
+																				id: creator.id,
+																			})}
+																		>
+																			<MetadataCreator
+																				name={creator.name}
+																				image={creator.image}
+																				character={creator.character}
+																			/>
+																		</Anchor>
+																	) : (
 																		<MetadataCreator
 																			name={creator.name}
 																			image={creator.image}
 																			character={creator.character}
 																		/>
-																	</Anchor>
-																) : (
-																	<MetadataCreator
-																		name={creator.name}
-																		image={creator.image}
-																		character={creator.character}
-																	/>
-																)}
-															</Box>
-														))}
-													</Flex>
-												</ScrollArea>
-											</Box>
-										))}
-									</Stack>
-								) : null}
-							</Stack>
-						</MediaScrollArea>
-					</Tabs.Panel>
-					<Tabs.Panel value="actions">
-						<MergeMetadataModal
-							onClose={mergeMetadataModalClose}
-							opened={mergeMetadataModalOpened}
-							metadataId={loaderData.metadataId}
-						/>
-						<MediaScrollArea>
-							<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
-								<Menu shadow="md">
-									<Menu.Target>
-										<Button variant="outline">Update progress</Button>
-									</Menu.Target>
-									<Menu.Dropdown>
-										{loaderData.metadataDetails.lot === MediaLot.Show ? (
-											<>
-												<Menu.Label>Shows</Menu.Label>
-												{nextEntry ? (
-													<>
-														<Menu.Item
-															onClick={() => {
-																setMetadataToUpdate({
-																	metadataId: loaderData.metadataId,
-																	showSeasonNumber: nextEntry.season,
-																	showEpisodeNumber: nextEntry.episode,
-																});
-															}}
-														>
-															Mark{" "}
-															{`S${nextEntry.season}-E${nextEntry.episode}`} as
-															seen
+																	)}
+																</Box>
+															))}
+														</Flex>
+													</ScrollArea>
+												</Box>
+											))}
+										</Stack>
+									) : null}
+								</Stack>
+							</MediaScrollArea>
+						</Tabs.Panel>
+						<Tabs.Panel value="actions">
+							<MergeMetadataModal
+								onClose={mergeMetadataModalClose}
+								opened={mergeMetadataModalOpened}
+								metadataId={loaderData.metadataId}
+							/>
+							<MediaScrollArea>
+								<SimpleGrid cols={{ base: 1, md: 2 }} spacing="lg">
+									<Menu shadow="md">
+										<Menu.Target>
+											<Button variant="outline">Update progress</Button>
+										</Menu.Target>
+										<Menu.Dropdown>
+											{loaderData.metadataDetails.lot === MediaLot.Show ? (
+												<>
+													<Menu.Label>Shows</Menu.Label>
+													{nextEntry ? (
+														<>
+															<Menu.Item
+																onClick={() => {
+																	setMetadataToUpdate({
+																		metadataId: loaderData.metadataId,
+																		showSeasonNumber: nextEntry.season,
+																		showEpisodeNumber: nextEntry.episode,
+																	});
+																}}
+															>
+																Mark{" "}
+																{`S${nextEntry.season}-E${nextEntry.episode}`}{" "}
+																as seen
+															</Menu.Item>
+															<PutOnHoldBtn />
+														</>
+													) : null}
+													{loaderData.userMetadataDetails.history.length !==
+													0 ? (
+														<DropBtn />
+													) : (
+														<Menu.Item disabled>
+															No history. Update from the seasons tab.
 														</Menu.Item>
-														<PutOnHoldBtn />
-													</>
-												) : null}
-												{loaderData.userMetadataDetails.history.length !== 0 ? (
-													<DropBtn />
-												) : (
-													<Menu.Item disabled>
-														No history. Update from the seasons tab.
+													)}
+												</>
+											) : null}
+											{loaderData.metadataDetails.lot === MediaLot.Anime &&
+											nextEntry ? (
+												<>
+													<Menu.Label>Anime</Menu.Label>
+													<Menu.Item
+														onClick={() => {
+															setMetadataToUpdate({
+																metadataId: loaderData.metadataId,
+																animeEpisodeNumber: nextEntry.episode,
+															});
+														}}
+													>
+														Mark EP-
+														{nextEntry.episode} as listened
 													</Menu.Item>
-												)}
-											</>
-										) : null}
-										{loaderData.metadataDetails.lot === MediaLot.Anime &&
-										nextEntry ? (
-											<>
-												<Menu.Label>Anime</Menu.Label>
-												<Menu.Item
-													onClick={() => {
-														setMetadataToUpdate({
-															metadataId: loaderData.metadataId,
-															animeEpisodeNumber: nextEntry.episode,
-														});
-													}}
-												>
-													Mark EP-
-													{nextEntry.episode} as listened
-												</Menu.Item>
-											</>
-										) : null}
-										{loaderData.metadataDetails.lot === MediaLot.Manga &&
-										nextEntry ? (
-											<>
-												<Menu.Label>Manga</Menu.Label>
-												<Menu.Item
-													onClick={() => {
-														setMetadataToUpdate({
-															metadataId: loaderData.metadataId,
-															mangaChapterNumber: nextEntry.chapter,
-															mangaVolumeNumber: nextEntry.volume,
-														});
-													}}
-												>
-													Mark{" "}
-													{nextEntry.chapter
-														? `CH-${nextEntry.chapter}`
-														: `VOL-${nextEntry.volume}`}{" "}
-													as read
-												</Menu.Item>
-											</>
-										) : null}
-										{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
-											<>
-												<Menu.Label>Podcasts</Menu.Label>
-												{nextEntry ? (
-													<>
-														<Menu.Item
-															onClick={() => {
-																setMetadataToUpdate({
-																	metadataId: loaderData.metadataId,
-																	podcastEpisodeNumber: nextEntry.episode,
-																});
-															}}
-														>
-															Mark EP-
-															{nextEntry.episode} as listened
-														</Menu.Item>
-														<PutOnHoldBtn />
-													</>
-												) : null}
-												{loaderData.userMetadataDetails.history.length !== 0 ? (
-													<DropBtn />
-												) : (
-													<Menu.Item disabled>
-														No history. Update from the episodes tab.
+												</>
+											) : null}
+											{loaderData.metadataDetails.lot === MediaLot.Manga &&
+											nextEntry ? (
+												<>
+													<Menu.Label>Manga</Menu.Label>
+													<Menu.Item
+														onClick={() => {
+															setMetadataToUpdate({
+																metadataId: loaderData.metadataId,
+																mangaChapterNumber: nextEntry.chapter,
+																mangaVolumeNumber: nextEntry.volume,
+															});
+														}}
+													>
+														Mark{" "}
+														{nextEntry.chapter
+															? `CH-${nextEntry.chapter}`
+															: `VOL-${nextEntry.volume}`}{" "}
+														as read
 													</Menu.Item>
-												)}
-											</>
-										) : null}
-										{loaderData.userMetadataDetails?.inProgress ? (
-											<>
-												<Menu.Label>In progress</Menu.Label>
-												<Menu.Item
-													onClick={() => {
-														setMetadataToUpdate({
-															metadataId: loaderData.metadataId,
-														});
-													}}
-												>
-													Set progress
-												</Menu.Item>
-												{loaderData.metadataDetails.lot !== MediaLot.Show &&
-												loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
-													<StateChangeButtons />
-												) : null}
-												<Form
-													replace
-													method="POST"
-													onSubmit={(e) => onSubmitProgressUpdate(e)}
-													action={withQuery($path("/actions"), {
-														intent: "individualProgressUpdate",
-													})}
-												>
-													<input hidden name="progress" defaultValue={100} />
-													<input
-														hidden
-														name="date"
-														defaultValue={formatDateToNaiveDate(new Date())}
-													/>
-													<input
-														hidden
-														name="metadataId"
-														defaultValue={loaderData.metadataId}
-													/>
-													<Menu.Item type="submit">I finished it</Menu.Item>
-												</Form>
-											</>
-										) : loaderData.metadataDetails.lot !== MediaLot.Show &&
-											loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
-											<>
-												<Menu.Label>Not in progress</Menu.Label>
-												<Form
-													replace
-													method="POST"
-													onSubmit={(e) => onSubmitProgressUpdate(e)}
-													action={withQuery($path("/actions"), {
-														intent: "individualProgressUpdate",
-													})}
-												>
-													<input hidden name="progress" defaultValue={0} />
-													<input
-														hidden
-														name="metadataId"
-														defaultValue={loaderData.metadataId}
-													/>
-													{![MediaLot.Anime, MediaLot.Manga].includes(
-														loaderData.metadataDetails.lot,
-													) ? (
-														<Menu.Item type="submit">
-															I'm{" "}
-															{getVerb(
-																Verb.Read,
-																loaderData.metadataDetails.lot,
-															)}
-															ing it
+												</>
+											) : null}
+											{loaderData.metadataDetails.lot === MediaLot.Podcast ? (
+												<>
+													<Menu.Label>Podcasts</Menu.Label>
+													{nextEntry ? (
+														<>
+															<Menu.Item
+																onClick={() => {
+																	setMetadataToUpdate({
+																		metadataId: loaderData.metadataId,
+																		podcastEpisodeNumber: nextEntry.episode,
+																	});
+																}}
+															>
+																Mark EP-
+																{nextEntry.episode} as listened
+															</Menu.Item>
+															<PutOnHoldBtn />
+														</>
+													) : null}
+													{loaderData.userMetadataDetails.history.length !==
+													0 ? (
+														<DropBtn />
+													) : (
+														<Menu.Item disabled>
+															No history. Update from the episodes tab.
 														</Menu.Item>
+													)}
+												</>
+											) : null}
+											{loaderData.userMetadataDetails?.inProgress ? (
+												<>
+													<Menu.Label>In progress</Menu.Label>
+													<Menu.Item
+														onClick={() => {
+															setMetadataToUpdate({
+																metadataId: loaderData.metadataId,
+															});
+														}}
+													>
+														Set progress
+													</Menu.Item>
+													{loaderData.metadataDetails.lot !== MediaLot.Show &&
+													loaderData.metadataDetails.lot !==
+														MediaLot.Podcast ? (
+														<StateChangeButtons />
 													) : null}
-												</Form>
-												<Menu.Item
-													onClick={() => {
-														setMetadataToUpdate({
-															metadataId: loaderData.metadataId,
-														});
-													}}
-												>
-													Add to{" "}
-													{getVerb(Verb.Read, loaderData.metadataDetails.lot)}{" "}
-													history
-												</Menu.Item>
-											</>
-										) : null}
-									</Menu.Dropdown>
-								</Menu>
-								{!userPreferences.general.disableReviews ? (
+													<Form
+														replace
+														method="POST"
+														onSubmit={(e) => onSubmitProgressUpdate(e)}
+														action={withQuery($path("/actions"), {
+															intent: "individualProgressUpdate",
+														})}
+													>
+														<input hidden name="progress" defaultValue={100} />
+														<input
+															hidden
+															name="date"
+															defaultValue={formatDateToNaiveDate(new Date())}
+														/>
+														<input
+															hidden
+															name="metadataId"
+															defaultValue={loaderData.metadataId}
+														/>
+														<Menu.Item type="submit">I finished it</Menu.Item>
+													</Form>
+												</>
+											) : loaderData.metadataDetails.lot !== MediaLot.Show &&
+												loaderData.metadataDetails.lot !== MediaLot.Podcast ? (
+												<>
+													<Menu.Label>Not in progress</Menu.Label>
+													<Form
+														replace
+														method="POST"
+														onSubmit={(e) => onSubmitProgressUpdate(e)}
+														action={withQuery($path("/actions"), {
+															intent: "individualProgressUpdate",
+														})}
+													>
+														<input hidden name="progress" defaultValue={0} />
+														<input
+															hidden
+															name="metadataId"
+															defaultValue={loaderData.metadataId}
+														/>
+														{![MediaLot.Anime, MediaLot.Manga].includes(
+															loaderData.metadataDetails.lot,
+														) ? (
+															<Menu.Item type="submit">
+																I'm{" "}
+																{getVerb(
+																	Verb.Read,
+																	loaderData.metadataDetails.lot,
+																)}
+																ing it
+															</Menu.Item>
+														) : null}
+													</Form>
+													<Menu.Item
+														onClick={() => {
+															setMetadataToUpdate({
+																metadataId: loaderData.metadataId,
+															});
+														}}
+													>
+														Add to{" "}
+														{getVerb(Verb.Read, loaderData.metadataDetails.lot)}{" "}
+														history
+													</Menu.Item>
+												</>
+											) : null}
+										</Menu.Dropdown>
+									</Menu>
+									{!userPreferences.general.disableReviews ? (
+										<Button
+											variant="outline"
+											w="100%"
+											onClick={() => {
+												setEntityToReview({
+													entityId: loaderData.metadataId,
+													entityLot: EntityLot.Metadata,
+													entityTitle: loaderData.metadataDetails.title,
+													metadataLot: loaderData.metadataDetails.lot,
+													existingReview: {
+														showExtraInformation: {
+															episode:
+																loaderData.userMetadataDetails?.nextEntry
+																	?.episode || undefined,
+															season:
+																loaderData.userMetadataDetails?.nextEntry
+																	?.season || undefined,
+														},
+														podcastExtraInformation: {
+															episode:
+																loaderData.userMetadataDetails?.nextEntry
+																	?.episode || undefined,
+														},
+														mangaExtraInformation: {
+															chapter:
+																loaderData.userMetadataDetails?.nextEntry
+																	?.chapter || undefined,
+														},
+														animeExtraInformation: {
+															episode:
+																loaderData.userMetadataDetails?.nextEntry
+																	?.episode || undefined,
+														},
+													},
+												});
+											}}
+										>
+											Post a review
+										</Button>
+									) : null}
 									<Button
 										variant="outline"
-										w="100%"
 										onClick={() => {
-											setEntityToReview({
+											setAddEntityToCollectionData({
 												entityId: loaderData.metadataId,
 												entityLot: EntityLot.Metadata,
-												entityTitle: loaderData.metadataDetails.title,
-												metadataLot: loaderData.metadataDetails.lot,
-												existingReview: {
-													showExtraInformation: {
-														episode:
-															loaderData.userMetadataDetails?.nextEntry
-																?.episode || undefined,
-														season:
-															loaderData.userMetadataDetails?.nextEntry
-																?.season || undefined,
-													},
-													podcastExtraInformation: {
-														episode:
-															loaderData.userMetadataDetails?.nextEntry
-																?.episode || undefined,
-													},
-													mangaExtraInformation: {
-														chapter:
-															loaderData.userMetadataDetails?.nextEntry
-																?.chapter || undefined,
-													},
-													animeExtraInformation: {
-														episode:
-															loaderData.userMetadataDetails?.nextEntry
-																?.episode || undefined,
-													},
-												},
+												alreadyInCollections:
+													loaderData.userMetadataDetails.collections.map(
+														(c) => c.id,
+													),
 											});
 										}}
 									>
-										Post a review
+										Add to collection
 									</Button>
-								) : null}
-								<Button
-									variant="outline"
-									onClick={() => {
-										setAddEntityToCollectionData({
-											entityId: loaderData.metadataId,
-											entityLot: EntityLot.Metadata,
-											alreadyInCollections:
-												loaderData.userMetadataDetails.collections.map(
-													(c) => c.id,
-												),
-										});
-									}}
-								>
-									Add to collection
-								</Button>
-								<Menu shadow="md">
-									<Menu.Target>
-										<Button variant="outline">More actions</Button>
-									</Menu.Target>
-									<Menu.Dropdown>
-										<ToggleMediaMonitorMenuItem
-											inCollections={loaderData.userMetadataDetails.collections.map(
-												(c) => c.name,
+									<Menu shadow="md">
+										<Menu.Target>
+											<Button variant="outline">More actions</Button>
+										</Menu.Target>
+										<Menu.Dropdown>
+											<ToggleMediaMonitorMenuItem
+												inCollections={loaderData.userMetadataDetails.collections.map(
+													(c) => c.name,
+												)}
+												formValue={loaderData.metadataId}
+												entityLot={EntityLot.Metadata}
+											/>
+											<Menu.Item onClick={mergeMetadataModalOpen}>
+												Merge media
+											</Menu.Item>
+											<Form
+												method="POST"
+												action={withQuery(".", { intent: "removeItem" })}
+											>
+												<input
+													hidden
+													name="metadataId"
+													defaultValue={loaderData.metadataId}
+												/>
+												<Menu.Item
+													color="red"
+													onClick={(e) => {
+														const form = e.currentTarget.form;
+														e.preventDefault();
+														openConfirmationModal(
+															"Are you sure you want to remove this item? This will remove it from all collections and delete all history and reviews.",
+															() => submit(form),
+														);
+													}}
+												>
+													Remove item
+												</Menu.Item>
+											</Form>
+										</Menu.Dropdown>
+									</Menu>
+									{canCurrentUserUpdate ? (
+										<Button
+											component={Link}
+											variant="outline"
+											to={$path(
+												"/media/update/:action",
+												{ action: "edit" },
+												{ id: loaderData.metadataDetails.id },
 											)}
-											formValue={loaderData.metadataId}
-											entityLot={EntityLot.Metadata}
-										/>
-										<Menu.Item onClick={mergeMetadataModalOpen}>
-											Merge media
-										</Menu.Item>
-										<Form
-											method="POST"
-											action={withQuery(".", { intent: "removeItem" })}
 										>
-											<input
-												hidden
-												name="metadataId"
-												defaultValue={loaderData.metadataId}
+											Edit metadata
+										</Button>
+									) : null}
+								</SimpleGrid>
+							</MediaScrollArea>
+						</Tabs.Panel>
+						<Tabs.Panel value="history">
+							{loaderData.userMetadataDetails.seenByAllCount > 0 ||
+							loaderData.userMetadataDetails.seenByUserCount > 0 ? (
+								<Stack h={MEDIA_DETAILS_HEIGHT} gap="xs">
+									<Box>
+										<Text fz={{ base: "sm", md: "md" }}>
+											Seen by all users{" "}
+											{loaderData.userMetadataDetails.seenByAllCount} time
+											{loaderData.userMetadataDetails.seenByAllCount > 1
+												? "s"
+												: ""}{" "}
+											and {loaderData.userMetadataDetails.seenByUserCount} time
+											{loaderData.userMetadataDetails &&
+											loaderData.userMetadataDetails.seenByUserCount > 1
+												? "s"
+												: ""}{" "}
+											by you.
+										</Text>
+									</Box>
+									<Virtuoso
+										data={loaderData.userMetadataDetails.history}
+										itemContent={(index, history) => (
+											<HistoryItem
+												index={index}
+												setTab={setTab}
+												key={history.id}
+												history={history}
+												reviewsVirtuosoRef={reviewsVirtuosoRef}
+												podcastVirtuosoRef={podcastVirtuosoRef}
 											/>
-											<Menu.Item
-												color="red"
-												onClick={async (e) => {
-													const form = e.currentTarget.form;
-													if (form) {
-														e.preventDefault();
-														const conf = await confirmWrapper({
-															confirmation:
-																"Are you sure you want to remove this item? This will remove it from all collections and delete all history and reviews.",
-														});
-														if (conf && form) submit(form);
-													}
-												}}
-											>
-												Remove item
-											</Menu.Item>
-										</Form>
-									</Menu.Dropdown>
-								</Menu>
-							</SimpleGrid>
-						</MediaScrollArea>
-					</Tabs.Panel>
-					<Tabs.Panel value="history">
-						{loaderData.userMetadataDetails.seenByAllCount > 0 ||
-						loaderData.userMetadataDetails.seenByUserCount > 0 ? (
-							<Stack h={MEDIA_DETAILS_HEIGHT} gap="xs">
-								<Box>
-									<Text fz={{ base: "sm", md: "md" }}>
-										Seen by all users{" "}
-										{loaderData.userMetadataDetails.seenByAllCount} time
-										{loaderData.userMetadataDetails.seenByAllCount > 1
-											? "s"
-											: ""}{" "}
-										and {loaderData.userMetadataDetails.seenByUserCount} time
-										{loaderData.userMetadataDetails &&
-										loaderData.userMetadataDetails.seenByUserCount > 1
-											? "s"
-											: ""}{" "}
-										by you.
-									</Text>
-								</Box>
-								<Virtuoso
-									data={loaderData.userMetadataDetails.history}
-									itemContent={(index, history) => (
-										<HistoryItem
-											index={index}
-											setTab={setTab}
-											key={history.id}
-											history={history}
-											reviewsVirtuosoRef={reviewsVirtuosoRef}
-											podcastVirtuosoRef={podcastVirtuosoRef}
-										/>
-									)}
-								/>
-							</Stack>
-						) : (
-							<Text>No history</Text>
-						)}
-					</Tabs.Panel>
-					<Tabs.Panel value="showSeasons">
-						<MediaScrollArea>
+										)}
+									/>
+								</Stack>
+							) : (
+								<Text>No history</Text>
+							)}
+						</Tabs.Panel>
+						<Tabs.Panel value="showSeasons" h={MEDIA_DETAILS_HEIGHT}>
 							{loaderData.metadataDetails.showSpecifics &&
 							loaderData.userMetadataDetails.showProgress ? (
-								<Accordion chevron={<Box />}>
-									{loaderData.metadataDetails.showSpecifics.seasons.map(
-										(season, seasonIdx) => {
-											const seasonProgress =
-												loaderData.userMetadataDetails.showProgress?.[
-													seasonIdx
-												];
-											return (
-												<Accordion.Item
-													key={season.seasonNumber}
-													value={season.seasonNumber.toString()}
-												>
-													<Accordion.Control component={Box} py={0} px="xs">
-														<DisplayShowSeason
-															season={season}
-															seasonProgress={seasonProgress}
-														/>
-													</Accordion.Control>
-													<Accordion.Panel>
-														<Stack h={300} gap="xs">
-															<Virtuoso
-																data={season.episodes}
-																itemContent={(episodeIdx, episode) => (
-																	<DisplayShowEpisode
-																		episode={episode}
-																		seasonIdx={seasonIdx}
-																		episodeIdx={episodeIdx}
-																		episodeProgress={
-																			seasonProgress?.episodes[episodeIdx]
-																		}
-																		seasonNumber={season.seasonNumber}
-																	/>
-																)}
-															/>
-														</Stack>
-													</Accordion.Panel>
-												</Accordion.Item>
-											);
-										},
-									)}
-								</Accordion>
+								<Virtuoso
+									data={loaderData.metadataDetails.showSpecifics.seasons}
+									itemContent={(seasonIdx, season) => {
+										return (
+											<DisplayShowSeason
+												season={season}
+												seasonIdx={seasonIdx}
+												key={season.seasonNumber}
+												openSeasonModal={() => setOpenedShowSeason(seasonIdx)}
+											/>
+										);
+									}}
+								/>
 							) : null}
-						</MediaScrollArea>
-					</Tabs.Panel>
-					{loaderData.metadataDetails.podcastSpecifics ? (
-						<Tabs.Panel value="podcastEpisodes" h={MEDIA_DETAILS_HEIGHT}>
-							<Virtuoso
-								ref={podcastVirtuosoRef}
-								data={loaderData.metadataDetails.podcastSpecifics.episodes}
-								itemContent={(podcastEpisodeIdx, podcastEpisode) => (
-									<DisplayPodcastEpisode
-										key={podcastEpisode.id}
-										episode={podcastEpisode}
-										index={podcastEpisodeIdx}
-										podcastProgress={
-											loaderData.userMetadataDetails.podcastProgress
-										}
-									/>
-								)}
-							/>
 						</Tabs.Panel>
-					) : null}
-					{!userPreferences.general.disableReviews ? (
-						<Tabs.Panel value="reviews" h={MEDIA_DETAILS_HEIGHT}>
-							{loaderData.userMetadataDetails.reviews.length > 0 ? (
+						{loaderData.metadataDetails.podcastSpecifics ? (
+							<Tabs.Panel value="podcastEpisodes" h={MEDIA_DETAILS_HEIGHT}>
 								<Virtuoso
-									ref={reviewsVirtuosoRef}
-									data={loaderData.userMetadataDetails.reviews}
-									itemContent={(_review, r) => (
-										<ReviewItemDisplay
-											key={r.id}
-											review={r}
-											entityLot={EntityLot.Metadata}
-											entityId={loaderData.metadataId}
-											lot={loaderData.metadataDetails.lot}
-											title={loaderData.metadataDetails.title}
+									ref={podcastVirtuosoRef}
+									data={loaderData.metadataDetails.podcastSpecifics.episodes}
+									itemContent={(podcastEpisodeIdx, podcastEpisode) => (
+										<DisplayPodcastEpisode
+											key={podcastEpisode.id}
+											episode={podcastEpisode}
+											index={podcastEpisodeIdx}
+											podcastProgress={
+												loaderData.userMetadataDetails.podcastProgress
+											}
 										/>
 									)}
 								/>
-							) : (
-								<Text>No reviews</Text>
-							)}
-						</Tabs.Panel>
-					) : null}
-					<Tabs.Panel value="suggestions" h={MEDIA_DETAILS_HEIGHT}>
-						{loaderData.metadataDetails.suggestions.length > 0 ? (
-							<VirtuosoGrid
-								components={{
-									List: forwardRef((props, ref) => (
-										<SimpleGrid
-											ref={ref}
-											{...props}
-											cols={{ base: 3, md: 4, lg: 5 }}
-										/>
-									)),
-								}}
-								totalCount={loaderData.metadataDetails.suggestions.length}
-								itemContent={(index) => (
-									<PartialMetadataDisplay
-										metadataId={loaderData.metadataDetails.suggestions[index]}
+							</Tabs.Panel>
+						) : null}
+						{!userPreferences.general.disableReviews ? (
+							<Tabs.Panel value="reviews" h={MEDIA_DETAILS_HEIGHT}>
+								{loaderData.userMetadataDetails.reviews.length > 0 ? (
+									<Virtuoso
+										ref={reviewsVirtuosoRef}
+										data={loaderData.userMetadataDetails.reviews}
+										itemContent={(_review, r) => (
+											<ReviewItemDisplay
+												key={r.id}
+												review={r}
+												entityLot={EntityLot.Metadata}
+												entityId={loaderData.metadataId}
+												lot={loaderData.metadataDetails.lot}
+												title={loaderData.metadataDetails.title}
+											/>
+										)}
 									/>
+								) : (
+									<Text>No reviews</Text>
 								)}
-							/>
-						) : (
-							<Text>No suggestions</Text>
-						)}
-					</Tabs.Panel>
-					{!userPreferences.general.disableVideos ? (
-						<Tabs.Panel value="videos">
-							<MediaScrollArea>
-								<Stack>
-									{loaderData.metadataDetails.assets.videos.map((v) => (
-										<VideoIframe
-											key={v.videoId}
-											videoId={v.videoId}
-											videoSource={v.source}
+							</Tabs.Panel>
+						) : null}
+						<Tabs.Panel value="suggestions" h={MEDIA_DETAILS_HEIGHT}>
+							{loaderData.metadataDetails.suggestions.length > 0 ? (
+								<VirtuosoGrid
+									components={{
+										List: forwardRef((props, ref) => (
+											<SimpleGrid
+												ref={ref}
+												{...props}
+												cols={{ base: 3, md: 4, lg: 5 }}
+											/>
+										)),
+									}}
+									totalCount={loaderData.metadataDetails.suggestions.length}
+									itemContent={(index) => (
+										<PartialMetadataDisplay
+											metadataId={loaderData.metadataDetails.suggestions[index]}
 										/>
-									))}
-								</Stack>
-							</MediaScrollArea>
+									)}
+								/>
+							) : (
+								<Text>No suggestions</Text>
+							)}
 						</Tabs.Panel>
-					) : null}
-					{!userPreferences.general.disableWatchProviders ? (
-						<Tabs.Panel value="watchProviders">
-							{loaderData.metadataDetails.watchProviders.length > 0 ? (
+						{!userPreferences.general.disableVideos ? (
+							<Tabs.Panel value="videos">
 								<MediaScrollArea>
-									<Stack gap="sm">
-										<Text>
-											JustWatch makes it easy to find out where you can legally
-											watch your favorite movies & TV shows online. Visit{" "}
-											<Anchor href={JUST_WATCH_URL}>JustWatch</Anchor> for more
-											information.
-										</Text>
-										<Text>
-											The following is a list of all available watch providers
-											for this media along with the countries they are available
-											in.
-										</Text>
-										{loaderData.metadataDetails.watchProviders.map(
-											(provider) => (
-												<Flex key={provider.name} align="center" gap="md">
-													<Image
-														src={provider.image}
-														h={80}
-														w={80}
-														radius="md"
-													/>
-													<Text lineClamp={3}>
-														{provider.name}:{" "}
-														<Text size="xs" span>
-															{provider.languages.join(", ")}
-														</Text>
-													</Text>
-												</Flex>
-											),
-										)}
+									<Stack>
+										{loaderData.metadataDetails.assets.videos.map((v) => (
+											<VideoIframe
+												key={v.videoId}
+												videoId={v.videoId}
+												videoSource={v.source}
+											/>
+										))}
 									</Stack>
 								</MediaScrollArea>
-							) : (
-								<Text>No watch providers</Text>
-							)}
-						</Tabs.Panel>
-					) : null}
-				</Tabs>
-			</MediaDetailsLayout>
-		</Container>
+							</Tabs.Panel>
+						) : null}
+						{!userPreferences.general.disableWatchProviders ? (
+							<Tabs.Panel value="watchProviders">
+								{loaderData.metadataDetails.watchProviders.length > 0 ? (
+									<MediaScrollArea>
+										<Stack gap="sm">
+											<Text>
+												JustWatch makes it easy to find out where you can
+												legally watch your favorite movies & TV shows online.
+												Visit <Anchor href={JUST_WATCH_URL}>JustWatch</Anchor>{" "}
+												for more information.
+											</Text>
+											<Text>
+												The following is a list of all available watch providers
+												for this media along with the countries they are
+												available in.
+											</Text>
+											{loaderData.metadataDetails.watchProviders.map(
+												(provider) => (
+													<Flex key={provider.name} align="center" gap="md">
+														<Image
+															src={provider.image}
+															h={80}
+															w={80}
+															radius="md"
+														/>
+														<Text lineClamp={3}>
+															{provider.name}:{" "}
+															<Text size="xs" span>
+																{provider.languages.join(", ")}
+															</Text>
+														</Text>
+													</Flex>
+												),
+											)}
+										</Stack>
+									</MediaScrollArea>
+								) : (
+									<Text>No watch providers</Text>
+								)}
+							</Tabs.Panel>
+						) : null}
+					</Tabs>
+				</MediaDetailsLayout>
+			</Container>
+		</>
 	);
 }
 
@@ -1236,6 +1235,56 @@ const VideoIframe = (props: {
 	);
 };
 
+const DisplayShowSeasonEpisodesModal = (props: {
+	openedShowSeason: number | undefined;
+	setOpenedShowSeason: (v: number | undefined) => void;
+}) => {
+	return (
+		<Modal
+			withCloseButton={false}
+			opened={props.openedShowSeason !== undefined}
+			onClose={() => props.setOpenedShowSeason(undefined)}
+		>
+			{isNumber(props.openedShowSeason) ? (
+				<DisplayShowSeasonEpisodes
+					openedShowSeason={props.openedShowSeason}
+					setOpenedShowSeason={props.setOpenedShowSeason}
+				/>
+			) : null}
+		</Modal>
+	);
+};
+
+const DisplayShowSeasonEpisodes = (props: {
+	openedShowSeason: number;
+	setOpenedShowSeason: (v: number | undefined) => void;
+}) => {
+	const loaderData = useLoaderData<typeof loader>();
+	const season =
+		loaderData.metadataDetails.showSpecifics?.seasons[props.openedShowSeason];
+	const seasonProgress =
+		loaderData.userMetadataDetails.showProgress?.[props.openedShowSeason];
+
+	return isNumber(props.openedShowSeason) && season ? (
+		<Stack h={600} gap="xs">
+			<Title order={3}>{getShowSeasonDisplayName(season)}</Title>
+			<Virtuoso
+				data={season.episodes}
+				itemContent={(episodeIdx, episode) => (
+					<DisplayShowEpisode
+						episode={episode}
+						episodeIdx={episodeIdx}
+						seasonNumber={season.seasonNumber}
+						seasonIdx={props.openedShowSeason}
+						episodeProgress={seasonProgress?.episodes[episodeIdx]}
+						beforeOpenModal={() => props.setOpenedShowSeason(undefined)}
+					/>
+				)}
+			/>
+		</Stack>
+	) : null;
+};
+
 const MetadataCreator = (props: {
 	name: string;
 	image?: string | null;
@@ -1597,14 +1646,13 @@ const HistoryItem = (props: {
 						<ActionIcon
 							color="red"
 							type="submit"
-							onClick={async (e) => {
+							onClick={(e) => {
 								const form = e.currentTarget.form;
 								e.preventDefault();
-								const conf = await confirmWrapper({
-									confirmation:
-										"Are you sure you want to delete this record from history?",
-								});
-								if (conf && form) submit(form);
+								openConfirmationModal(
+									"Are you sure you want to delete this record from history?",
+									() => submit(form),
+								);
 							}}
 						>
 							<IconX size={20} />
@@ -1701,6 +1749,7 @@ const DisplaySeasonOrEpisodeDetails = (props: {
 	runtime?: number | null;
 	overview?: string | null;
 	displayIndicator: number;
+	onNameClick?: () => void;
 	id?: number | string | null;
 	numEpisodes?: number | null;
 	posterImages: Array<string>;
@@ -1735,7 +1784,13 @@ const DisplaySeasonOrEpisodeDetails = (props: {
 
 	const DisplayDetails = () => (
 		<>
-			<Text lineClamp={2}>{props.name}</Text>
+			{props.onNameClick ? (
+				<Anchor onClick={props.onNameClick} lineClamp={2}>
+					{props.name}
+				</Anchor>
+			) : (
+				<Text lineClamp={2}>{props.name}</Text>
+			)}
 			{display ? (
 				<Flex align="center" gap={4}>
 					{display}
@@ -1800,50 +1855,60 @@ type SeasonProgress = NonNullable<
 	UserMetadataDetailsQuery["userMetadataDetails"]["showProgress"]
 >[number];
 
+const getShowSeasonDisplayName = (season: Season) =>
+	`${season.seasonNumber}. ${season.name}`;
+
 const DisplayShowSeason = (props: {
 	season: Season;
+	seasonIdx: number;
+	openSeasonModal: () => void;
 	seasonProgress?: SeasonProgress;
 }) => {
 	const loaderData = useLoaderData<typeof loader>();
 	const [_, setMetadataToUpdate] = useMetadataProgressUpdate();
+
 	const numTimesSeen = props.seasonProgress?.timesSeen || 0;
 	const isSeen = numTimesSeen > 0;
 
 	return (
-		<DisplaySeasonOrEpisodeDetails
-			{...props.season}
-			name={`${props.season.seasonNumber}. ${props.season.name}`}
-			numEpisodes={props.season.episodes.length}
-			displayIndicator={numTimesSeen}
-			runtime={props.season.episodes
-				.map((e) => e.runtime || 0)
-				.reduce((i, a) => i + a, 0)}
-		>
-			{props.season.episodes.length > 0 ? (
-				<Button
-					variant={isSeen ? "default" : "outline"}
-					size="xs"
-					color="blue"
-					onClick={() => {
-						setMetadataToUpdate({
-							metadataId: loaderData.metadataId,
-							showSeasonNumber: props.season.seasonNumber,
-							showEpisodeNumber: props.season.episodes.at(-1)?.episodeNumber,
-							showAllEpisodesBefore: true,
-						});
-					}}
-				>
-					{isSeen ? "Watch again" : "Mark as seen"}
-				</Button>
-			) : null}
-		</DisplaySeasonOrEpisodeDetails>
+		<Box my={props.seasonIdx !== 0 ? "md" : undefined}>
+			<DisplaySeasonOrEpisodeDetails
+				{...props.season}
+				displayIndicator={numTimesSeen}
+				numEpisodes={props.season.episodes.length}
+				onNameClick={() => props.openSeasonModal()}
+				name={getShowSeasonDisplayName(props.season)}
+				runtime={props.season.episodes
+					.map((e) => e.runtime || 0)
+					.reduce((i, a) => i + a, 0)}
+			>
+				{props.season.episodes.length > 0 ? (
+					<Button
+						variant={isSeen ? "default" : "outline"}
+						size="xs"
+						color="blue"
+						onClick={() => {
+							setMetadataToUpdate({
+								metadataId: loaderData.metadataId,
+								showSeasonNumber: props.season.seasonNumber,
+								showEpisodeNumber: props.season.episodes.at(-1)?.episodeNumber,
+								showAllEpisodesBefore: true,
+							});
+						}}
+					>
+						{isSeen ? "Watch again" : "Mark as seen"}
+					</Button>
+				) : null}
+			</DisplaySeasonOrEpisodeDetails>
+		</Box>
 	);
 };
 
 const DisplayShowEpisode = (props: {
 	seasonIdx: number;
-	seasonNumber: number;
 	episodeIdx: number;
+	seasonNumber: number;
+	beforeOpenModal?: () => void;
 	episode: Season["episodes"][number];
 	episodeProgress?: SeasonProgress["episodes"][number];
 }) => {
@@ -1861,10 +1926,11 @@ const DisplayShowEpisode = (props: {
 				displayIndicator={numTimesEpisodeSeen}
 			>
 				<Button
-					variant={numTimesEpisodeSeen > 0 ? "default" : "outline"}
 					size="xs"
 					color="blue"
+					variant={numTimesEpisodeSeen > 0 ? "default" : "outline"}
 					onClick={() => {
+						if (props.beforeOpenModal) props.beforeOpenModal();
 						setMetadataToUpdate({
 							metadataId: loaderData.metadataId,
 							showSeasonNumber: props.seasonNumber,
diff --git a/apps/frontend/app/routes/_dashboard.media.update.$action.tsx b/apps/frontend/app/routes/_dashboard.media.update.$action.tsx
new file mode 100644
index 0000000000..ba1ba470e7
--- /dev/null
+++ b/apps/frontend/app/routes/_dashboard.media.update.$action.tsx
@@ -0,0 +1,262 @@
+import {
+	Anchor,
+	Button,
+	Code,
+	Container,
+	FileInput,
+	Group,
+	JsonInput,
+	NumberInput,
+	Select,
+	Stack,
+	Switch,
+	TextInput,
+	Textarea,
+	Title,
+} from "@mantine/core";
+import {
+	type ActionFunctionArgs,
+	type LoaderFunctionArgs,
+	type MetaArgs,
+	redirect,
+	unstable_parseMultipartFormData,
+} from "@remix-run/node";
+import { Form, useLoaderData } from "@remix-run/react";
+import {
+	CreateCustomMetadataDocument,
+	MediaLot,
+	MetadataDetailsDocument,
+	UpdateCustomMetadataDocument,
+} from "@ryot/generated/graphql/backend/graphql";
+import { camelCase, changeCase, processSubmission } from "@ryot/ts-utils";
+import { IconCalendar, IconPhoto, IconVideo } from "@tabler/icons-react";
+import { $path } from "remix-routes";
+import invariant from "tiny-invariant";
+import { match } from "ts-pattern";
+import { z } from "zod";
+import { zx } from "zodix";
+import { useCoreDetails } from "~/lib/hooks";
+import { s3FileUploader, serverGqlService } from "~/lib/utilities.server";
+
+enum Action {
+	Create = "create",
+	Edit = "edit",
+}
+
+const searchParamsSchema = z.object({
+	id: z.string().optional(),
+	lot: z.nativeEnum(MediaLot).optional(),
+});
+
+export type SearchParams = z.infer<typeof searchParamsSchema>;
+
+export const loader = async ({ params, request }: LoaderFunctionArgs) => {
+	const { action } = zx.parseParams(params, { action: z.nativeEnum(Action) });
+	const query = zx.parseQuery(request, searchParamsSchema);
+	const details = await match(action)
+		.with(Action.Create, () => undefined)
+		.with(Action.Edit, async () => {
+			invariant(query.id);
+			const { metadataDetails } = await serverGqlService.authenticatedRequest(
+				request,
+				MetadataDetailsDocument,
+				{ metadataId: query.id },
+			);
+			return metadataDetails;
+		})
+		.exhaustive();
+	return { query, action, details };
+};
+
+export const meta = (_args: MetaArgs<typeof loader>) => {
+	return [{ title: "Create Media | Ryot" }];
+};
+
+export const action = async ({ request }: ActionFunctionArgs) => {
+	const uploaders = s3FileUploader("metadata");
+	const formData = await unstable_parseMultipartFormData(request, uploaders);
+	const submission = processSubmission(formData, schema);
+	// biome-ignore lint/suspicious/noExplicitAny: required here
+	const input: any = {
+		...submission,
+		[`${camelCase(submission.lot)}Specifics`]: submission.specifics
+			? JSON.parse(submission.specifics)
+			: undefined,
+	};
+	input.id = undefined;
+	input.action = undefined;
+	input.specifics = undefined;
+	input.genres = input.genres?.split(",");
+	input.creators = input.creators?.split(",");
+	const id = await match(submission.action)
+		.with(Action.Create, async () => {
+			const { createCustomMetadata } =
+				await serverGqlService.authenticatedRequest(
+					request,
+					CreateCustomMetadataDocument,
+					{ input },
+				);
+			return createCustomMetadata.id;
+		})
+		.with(Action.Edit, async () => {
+			invariant(submission.id);
+			await serverGqlService.authenticatedRequest(
+				request,
+				UpdateCustomMetadataDocument,
+				{ input: { existingMetadataId: submission.id, update: input } },
+			);
+			return submission.id;
+		})
+		.exhaustive();
+	return redirect($path("/media/item/:id", { id }));
+};
+
+const optionalString = z.string().optional();
+const optionalStringArray = z.array(z.string()).optional();
+
+const schema = z.object({
+	title: z.string(),
+	id: optionalString,
+	genres: optionalString,
+	creators: optionalString,
+	specifics: optionalString,
+	images: optionalStringArray,
+	videos: optionalStringArray,
+	description: optionalString,
+	action: z.nativeEnum(Action),
+	isNsfw: z.boolean().optional(),
+	lot: z.nativeEnum(MediaLot),
+	publishYear: z.number().optional(),
+});
+
+export default function Page() {
+	const loaderData = useLoaderData<typeof loader>();
+	const coreDetails = useCoreDetails();
+	const fileUploadNotAllowed = !coreDetails.fileStorageEnabled;
+
+	return (
+		<Container>
+			<Form method="POST" encType="multipart/form-data">
+				<input hidden name="action" defaultValue={loaderData.action} />
+				{loaderData.details ? (
+					<input hidden name="id" defaultValue={loaderData.details.id} />
+				) : null}
+				<Stack>
+					<Title>
+						{loaderData.details
+							? `Updating ${loaderData.details.title}`
+							: "Create Media"}
+					</Title>
+					<TextInput
+						required
+						autoFocus
+						name="title"
+						label="Title"
+						defaultValue={loaderData.details?.title}
+					/>
+					<Group wrap="nowrap">
+						<Select
+							required
+							name="lot"
+							label="Type"
+							defaultValue={loaderData.details?.lot || loaderData.query.lot}
+							data={Object.values(MediaLot).map((v) => ({
+								value: v,
+								label: changeCase(v),
+							}))}
+						/>
+						<Switch
+							mt="md"
+							name="isNsfw"
+							label="Is it NSFW?"
+							defaultChecked={loaderData.details?.isNsfw || undefined}
+						/>
+					</Group>
+					<JsonInput
+						formatOnBlur
+						name="specifics"
+						label="Specifics"
+						defaultValue={JSON.stringify(
+							loaderData.details?.movieSpecifics ||
+								loaderData.details?.showSpecifics ||
+								loaderData.details?.mangaSpecifics ||
+								loaderData.details?.animeSpecifics ||
+								loaderData.details?.podcastSpecifics ||
+								loaderData.details?.bookSpecifics ||
+								loaderData.details?.audioBookSpecifics ||
+								loaderData.details?.visualNovelSpecifics ||
+								loaderData.details?.videoGameSpecifics ||
+								loaderData.details?.musicSpecifics,
+						)}
+						description={
+							<>
+								Please search for <Code>Specifics</Code> inputs at the{" "}
+								<Anchor href="/backend/graphql" size="xs" target="_blank">
+									graphql endpoint
+								</Anchor>{" "}
+								for the required JSON structure
+							</>
+						}
+					/>
+					<Textarea
+						label="Description"
+						name="description"
+						description="Markdown is supported"
+						defaultValue={loaderData.details?.description || undefined}
+					/>
+					{!fileUploadNotAllowed ? (
+						<FileInput
+							multiple
+							name="images"
+							label="Images"
+							leftSection={<IconPhoto />}
+							accept="image/png,image/jpeg,image/jpg"
+							description={
+								loaderData.details &&
+								"Please re-upload the images while updating the metadata, old ones will be deleted"
+							}
+						/>
+					) : null}
+					{!fileUploadNotAllowed ? (
+						<FileInput
+							multiple
+							name="videos"
+							label="Videos"
+							leftSection={<IconVideo />}
+							accept="video/mp4,video/x-m4v,video/*"
+							description={
+								loaderData.details &&
+								"Please re-upload the videos while updating the metadata, old ones will be deleted"
+							}
+						/>
+					) : null}
+					<NumberInput
+						name="publishYear"
+						label="Publish year"
+						leftSection={<IconCalendar />}
+						defaultValue={loaderData.details?.publishYear || undefined}
+					/>
+					<TextInput
+						name="creators"
+						label="Creators"
+						placeholder="Comma separated names"
+						defaultValue={loaderData.details?.creators
+							.flatMap((c) => c.items)
+							.map((c) => c.name)
+							.join(", ")}
+					/>
+					<TextInput
+						name="genres"
+						label="Genres"
+						placeholder="Comma separated values"
+						defaultValue={
+							loaderData.details?.genres.map((g) => g.name).join(", ") ||
+							undefined
+						}
+					/>
+					<Button type="submit">Create</Button>
+				</Stack>
+			</Form>
+		</Container>
+	);
+}
diff --git a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
index f7f4808353..a8ed6474c4 100644
--- a/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.imports-and-exports._index.tsx
@@ -46,8 +46,7 @@ import { useState } from "react";
 import { match } from "ts-pattern";
 import { withFragment, withQuery } from "ufo";
 import { z } from "zod";
-import { confirmWrapper } from "~/components/confirmation";
-import { dayjsLib } from "~/lib/generals";
+import { dayjsLib, openConfirmationModal } from "~/lib/generals";
 import {
 	useApplicationEvents,
 	useConfirmSubmit,
@@ -398,14 +397,13 @@ export default function Page() {
 											color="blue"
 											type="submit"
 											variant="light"
-											onClick={async (e) => {
+											onClick={(e) => {
 												const form = e.currentTarget.form;
 												e.preventDefault();
-												const conf = await confirmWrapper({
-													confirmation:
-														"Are you sure you want to deploy an import job? This action is irreversible.",
-												});
-												if (conf && form) submit(form);
+												openConfirmationModal(
+													"Are you sure you want to deploy an import job? This action is irreversible.",
+													() => submit(form),
+												);
 											}}
 										>
 											Import
diff --git a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
index 892f7c9df1..bb504ec7c8 100644
--- a/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.integrations.tsx
@@ -51,12 +51,12 @@ import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
 import {
 	PRO_REQUIRED_MESSAGE,
 	applicationBaseUrl,
 	commaDelimitedString,
 	dayjsLib,
+	openConfirmationModal,
 } from "~/lib/generals";
 import {
 	useConfirmSubmit,
@@ -372,14 +372,13 @@ const DisplayIntegration = (props: {
 								color="red"
 								variant="subtle"
 								mt={4}
-								onClick={async (e) => {
+								onClick={(e) => {
 									const form = e.currentTarget.form;
 									e.preventDefault();
-									const conf = await confirmWrapper({
-										confirmation:
-											"Are you sure you want to delete this integration?",
-									});
-									if (conf && form) submit(form);
+									openConfirmationModal(
+										"Are you sure you want to delete this integration?",
+										() => submit(form),
+									);
 								}}
 							>
 								<IconTrash />
diff --git a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
index d27d54f050..0307fb8609 100644
--- a/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.miscellaneous.tsx
@@ -17,7 +17,12 @@ import {
 import { processSubmission } from "@ryot/ts-utils";
 import { match } from "ts-pattern";
 import { z } from "zod";
-import { useDashboardLayoutData, useUserDetails } from "~/lib/hooks";
+import { openConfirmationModal } from "~/lib/generals";
+import {
+	useConfirmSubmit,
+	useDashboardLayoutData,
+	useUserDetails,
+} from "~/lib/hooks";
 import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 
 export const meta = (_args: MetaArgs) => {
@@ -47,29 +52,23 @@ const jobSchema = z.object({
 export default function Page() {
 	return (
 		<Container size="lg">
-			<Form replace method="POST">
-				<Stack>
-					<Title>Miscellaneous settings</Title>
-					<SimpleGrid
-						cols={{ base: 1, lg: 2 }}
-						spacing={{ base: "xl", md: "md" }}
-					>
-						{Object.values(BackgroundJob).map((job) => (
-							<DisplayJobBtn key={job} job={job} />
-						))}
-					</SimpleGrid>
-				</Stack>
-			</Form>
+			<Stack>
+				<Title>Miscellaneous settings</Title>
+				<SimpleGrid
+					cols={{ base: 1, lg: 2 }}
+					spacing={{ base: "xl", md: "md" }}
+				>
+					{Object.values(BackgroundJob).map((job) => (
+						<DisplayJobBtn key={job} job={job} />
+					))}
+				</SimpleGrid>
+			</Stack>
 		</Container>
 	);
 }
 
-const DisplayJobBtn = (props: { job: BackgroundJob }) => {
-	const userDetails = useUserDetails();
-	const dashboardData = useDashboardLayoutData();
-	const isEditDisabled = dashboardData.isDemo;
-
-	const [title, description, isAdminOnly] = match(props.job)
+const getJobDetails = (job: BackgroundJob) =>
+	match(job)
 		.with(
 			BackgroundJob.UpdateAllMetadata,
 			() =>
@@ -132,24 +131,41 @@ const DisplayJobBtn = (props: { job: BackgroundJob }) => {
 		)
 		.exhaustive();
 
+const DisplayJobBtn = (props: { job: BackgroundJob }) => {
+	const userDetails = useUserDetails();
+	const dashboardData = useDashboardLayoutData();
+	const isEditDisabled = dashboardData.isDemo;
+	const submit = useConfirmSubmit();
+
+	const [title, description, isAdminOnly] = getJobDetails(props.job);
+
 	if (isAdminOnly && userDetails.lot !== UserLot.Admin) return null;
 
 	return (
-		<Stack>
-			<Box>
-				<Title order={4}>{title}</Title>
-				<Text>{description}</Text>
-			</Box>
-			<Button
-				mt="auto"
-				type="submit"
-				name="jobName"
-				variant="light"
-				value={props.job}
-				disabled={isEditDisabled}
-			>
-				{title}
-			</Button>
-		</Stack>
+		<Form replace method="POST">
+			<input hidden name="jobName" defaultValue={props.job} />
+			<Stack>
+				<Box>
+					<Title order={4}>{title}</Title>
+					<Text>{description}</Text>
+				</Box>
+				<Button
+					mt="auto"
+					type="submit"
+					variant="light"
+					disabled={isEditDisabled}
+					onClick={(e) => {
+						const form = e.currentTarget.form;
+						e.preventDefault();
+						openConfirmationModal(
+							"Are you sure you want to perform this task?",
+							() => submit(form),
+						);
+					}}
+				>
+					{title}
+				</Button>
+			</Stack>
+		</Form>
 	);
 };
diff --git a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
index 3b2d003706..47738ae6b5 100644
--- a/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.notifications.tsx
@@ -44,8 +44,7 @@ import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
-import { dayjsLib } from "~/lib/generals";
+import { dayjsLib, openConfirmationModal } from "~/lib/generals";
 import { useConfirmSubmit, useCoreDetails } from "~/lib/hooks";
 import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 
@@ -379,14 +378,13 @@ const DisplayNotification = (props: {
 								type="submit"
 								color="red"
 								variant="subtle"
-								onClick={async (e) => {
+								onClick={(e) => {
 									const form = e.currentTarget.form;
 									e.preventDefault();
-									const conf = await confirmWrapper({
-										confirmation:
-											"Are you sure you want to delete this notification platform?",
-									});
-									if (conf && form) submit(form);
+									openConfirmationModal(
+										"Are you sure you want to delete this notification platform?",
+										() => submit(form),
+									);
 								}}
 							>
 								<IconTrash />
diff --git a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
index c4b973a2a6..7751988158 100644
--- a/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.preferences.tsx
@@ -20,55 +20,41 @@ import {
 	TagsInput,
 	Text,
 	Title,
-	Tooltip,
 	rem,
 } from "@mantine/core";
 import { notifications } from "@mantine/notifications";
-import type {
-	ActionFunctionArgs,
-	LoaderFunctionArgs,
-	MetaArgs,
-} from "@remix-run/node";
-import { Form, data, useLoaderData } from "@remix-run/react";
+import type { LoaderFunctionArgs, MetaArgs } from "@remix-run/node";
+import { useLoaderData, useRevalidator } from "@remix-run/react";
 import {
 	DashboardElementLot,
 	GridPacking,
 	MediaLot,
 	MediaStateChanged,
 	UpdateUserPreferenceDocument,
+	type UserPreferences,
 	UserReviewScale,
 	UserUnitSystem,
 } from "@ryot/generated/graphql/backend/graphql";
-import {
-	camelCase,
-	changeCase,
-	cn,
-	isNumber,
-	snakeCase,
-	startCase,
-} from "@ryot/ts-utils";
+import { changeCase, cn, isNumber, snakeCase, startCase } from "@ryot/ts-utils";
 import { IconCheckbox } from "@tabler/icons-react";
 import {
 	IconAlertCircle,
 	IconBellRinging,
 	IconGripVertical,
-	IconRotate360,
 } from "@tabler/icons-react";
+import { useMutation } from "@tanstack/react-query";
+import { type Draft, produce } from "immer";
 import { Fragment, useState } from "react";
 import { match } from "ts-pattern";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
-import { PRO_REQUIRED_MESSAGE } from "~/lib/generals";
+import { PRO_REQUIRED_MESSAGE, clientGqlService } from "~/lib/generals";
 import {
-	useComplexJsonUpdate,
-	useConfirmSubmit,
 	useCoreDetails,
 	useDashboardLayoutData,
 	useIsFitnessActionActive,
 	useUserPreferences,
 } from "~/lib/hooks";
-import { createToastHeaders, serverGqlService } from "~/lib/utilities.server";
 import classes from "~/styles/preferences.module.css";
 
 const searchSchema = z.object({
@@ -91,123 +77,76 @@ const notificationContent = {
 		"Changing preferences is disabled for demo users. Please create an account to save your preferences.",
 };
 
-export const action = async ({ request }: ActionFunctionArgs) => {
-	const entries = Object.entries(Object.fromEntries(await request.formData()));
-	const submission = [];
-	for (let [property, value] of entries) {
-		if (property === "reset") {
-			property = "";
-			value = "";
-		}
-		submission.push({
-			property,
-			value: value.toString(),
-		});
-	}
-	for (const input of submission) {
-		await serverGqlService.authenticatedRequest(
-			request,
-			UpdateUserPreferenceDocument,
-			{ input },
-		);
-	}
-	const toastHeaders = await createToastHeaders({
-		message: "Preferences updated",
-		type: "success",
-	});
-	return data({}, { headers: toastHeaders });
-};
+type UpdatePreferenceFunc = (draft: Draft<UserPreferences>) => void;
 
 export default function Page() {
 	const loaderData = useLoaderData<typeof loader>();
 	const coreDetails = useCoreDetails();
 	const userPreferences = useUserPreferences();
-	const submit = useConfirmSubmit();
+	const revalidator = useRevalidator();
 	const isFitnessActionActive = useIsFitnessActionActive();
-	const [watchProviders, setWatchProviders] = useState(
-		userPreferences.general.watchProviders.map((wp) => ({
-			...wp,
-			lot: snakeCase(wp.lot),
-		})),
-	);
-	const [dashboardElements, setDashboardElements] = useState(
-		userPreferences.general.dashboard,
-	);
-	const { toUpdatePreferences, appendPref, reset } = useComplexJsonUpdate();
 	const [defaultTab, setDefaultTab] = useState(
 		loaderData.query.defaultTab || "dashboard",
 	);
 	const dashboardData = useDashboardLayoutData();
+	const [changingUserPreferences, setChangingUserPreferences] = useState({
+		isChanged: false,
+		value: userPreferences,
+	});
 	const isEditDisabled = dashboardData.isDemo;
 
+	const updateUserPreferencesMutation = useMutation({
+		mutationFn: async () => {
+			await clientGqlService.request(UpdateUserPreferenceDocument, {
+				input: changingUserPreferences.value,
+			});
+			await new Promise((r) => setTimeout(r, 1000));
+			revalidator.revalidate();
+		},
+	});
+
+	const updatePreference = (makeChange: UpdatePreferenceFunc) => {
+		setChangingUserPreferences(
+			produce(changingUserPreferences, (draft) => {
+				draft.isChanged = true;
+				makeChange(draft.value);
+			}),
+		);
+	};
+
 	return (
 		<Container size="xs">
-			{toUpdatePreferences.length > 0 ? (
+			{changingUserPreferences.isChanged ? (
 				<Affix
 					position={{
 						bottom: rem(45),
 						right: rem(isFitnessActionActive ? 100 : 40),
 					}}
 				>
-					<Form
-						replace
-						method="POST"
-						action={`?defaultTab=${defaultTab}`}
-						onSubmit={() => reset()}
+					<Button
+						color="green"
+						variant="outline"
+						leftSection={<IconCheckbox size={20} />}
+						loading={updateUserPreferencesMutation.isPending}
+						onClick={async () => {
+							await updateUserPreferencesMutation.mutateAsync();
+							notifications.show({
+								color: "green",
+								title: "Preferences updated",
+								message: "Preferences have been updated.",
+							});
+							setChangingUserPreferences({
+								isChanged: false,
+								value: userPreferences,
+							});
+						}}
 					>
-						{toUpdatePreferences.map((pref) => (
-							<input
-								key={pref[0]}
-								hidden
-								name={pref[0]}
-								value={pref[1]}
-								readOnly
-							/>
-						))}
-						<Button
-							color="green"
-							variant="outline"
-							leftSection={<IconCheckbox size={20} />}
-							type="submit"
-						>
-							Save changes ({toUpdatePreferences.length})
-						</Button>
-					</Form>
+						Save changes
+					</Button>
 				</Affix>
 			) : null}
 			<Stack>
-				<Group justify="space-between">
-					<Title>Preferences</Title>
-					<Form method="POST" reloadDocument>
-						<input type="hidden" name="reset" defaultValue="reset" />
-						<Tooltip label="Reset preferences">
-							<ActionIcon
-								color="red"
-								type="submit"
-								variant="outline"
-								onClick={async (e) => {
-									if (!isEditDisabled) {
-										const form = e.currentTarget.form;
-										e.preventDefault();
-										const conf = await confirmWrapper({
-											confirmation:
-												"This will reset all your preferences to default. Are you sure you want to continue?",
-										});
-										if (conf && form) submit(form);
-										else
-											notifications.show({
-												message:
-													"Preferences have been reset. Please reload the page.",
-												color: "green",
-											});
-									} else notifications.show(notificationContent);
-								}}
-							>
-								<IconRotate360 size={20} />
-							</ActionIcon>
-						</Tooltip>
-					</Form>
-				</Group>
+				<Title>Preferences</Title>
 				{isEditDisabled ? (
 					<Alert icon={<IconAlertCircle />} variant="outline" color="violet">
 						{notificationContent.message}
@@ -234,27 +173,32 @@ export default function Page() {
 						<DragDropContext
 							onDragEnd={({ destination, source }) => {
 								if (!isEditDisabled) {
-									const newOrder = reorder(dashboardElements, {
+									const newOrder = reorder(userPreferences.general.dashboard, {
 										from: source.index,
 										to: destination?.index || 0,
 									});
-									setDashboardElements(newOrder);
-									appendPref("general.dashboard", JSON.stringify(newOrder));
-								} else notifications.show(notificationContent);
+									updatePreference((draft) => {
+										draft.general.dashboard = newOrder;
+									});
+								} else {
+									notifications.show(notificationContent);
+								}
 							}}
 						>
 							<Droppable droppableId="dnd-list">
 								{(provided) => (
 									<Stack {...provided.droppableProps} ref={provided.innerRef}>
-										{dashboardElements.map((de, index) => (
-											<EditDashboardElement
-												index={index}
-												key={de.section}
-												lot={de.section}
-												appendPref={appendPref}
-												isEditDisabled={isEditDisabled}
-											/>
-										))}
+										{changingUserPreferences.value.general.dashboard.map(
+											(de, index) => (
+												<EditDashboardElement
+													index={index}
+													key={de.section}
+													lot={de.section}
+													isEditDisabled={isEditDisabled}
+													updatePreference={updatePreference}
+												/>
+											),
+										)}
 										{provided.placeholder}
 									</Stack>
 								)}
@@ -279,11 +223,11 @@ export default function Page() {
 													defaultChecked={isEnabled}
 													disabled={!!isEditDisabled}
 													onChange={(ev) => {
-														const lot = snakeCase(name);
-														appendPref(
-															`features_enabled.${facet}.${lot}`,
-															String(ev.currentTarget.checked),
-														);
+														updatePreference((draft) => {
+															// biome-ignore lint/suspicious/noExplicitAny: too much work to use correct types
+															(draft as any).featuresEnabled[facet][name] =
+																ev.currentTarget.checked;
+														});
 													}}
 												/>
 											))}
@@ -308,8 +252,15 @@ export default function Page() {
 									] as const
 								).map((name) => (
 									<Switch
-										key={name}
 										size="xs"
+										key={name}
+										disabled={!!isEditDisabled}
+										defaultChecked={userPreferences.general[name]}
+										onChange={(ev) => {
+											updatePreference((draft) => {
+												draft.general[name] = ev.currentTarget.checked;
+											});
+										}}
 										label={match(name)
 											.with(
 												"displayNsfw",
@@ -334,27 +285,23 @@ export default function Page() {
 												() => "Persist queries in the URL",
 											)
 											.exhaustive()}
-										defaultChecked={userPreferences.general[name]}
-										disabled={!!isEditDisabled}
-										onChange={(ev) => {
-											appendPref(
-												`general.${snakeCase(name)}`,
-												String(ev.currentTarget.checked),
-											);
-										}}
 									/>
 								))}
 								<Select
 									size="xs"
+									disabled={!!isEditDisabled}
 									label="Scale used for rating in reviews"
+									defaultValue={userPreferences.general.reviewScale}
 									data={Object.values(UserReviewScale).map((c) => ({
-										label: startCase(snakeCase(c)),
 										value: c,
+										label: startCase(snakeCase(c)),
 									}))}
-									defaultValue={userPreferences.general.reviewScale}
-									disabled={!!isEditDisabled}
 									onChange={(val) => {
-										if (val) appendPref("general.review_scale", val);
+										if (val) {
+											updatePreference((draft) => {
+												draft.general.reviewScale = val as UserReviewScale;
+											});
+										}
 									}}
 								/>
 							</SimpleGrid>
@@ -365,55 +312,60 @@ export default function Page() {
 								<SegmentedControl
 									mt="xs"
 									fullWidth
+									disabled={!!isEditDisabled}
+									defaultValue={userPreferences.general.gridPacking}
 									data={Object.values(GridPacking).map((c) => ({
-										label: startCase(snakeCase(c)),
 										value: c,
+										label: startCase(snakeCase(c)),
 									}))}
-									defaultValue={userPreferences.general.gridPacking}
-									disabled={!!isEditDisabled}
 									onChange={(val) => {
-										if (val) appendPref("general.grid_packing", val);
+										if (val) {
+											updatePreference((draft) => {
+												draft.general.gridPacking = val as GridPacking;
+											});
+										}
 									}}
 								/>
 							</Input.Wrapper>
 							<Stack>
 								<Title order={3}>Watch providers</Title>
-								{Object.values(MediaLot)
-									.map(snakeCase)
-									.map((lot) => {
-										const existingValues =
-											watchProviders.find((wp) => wp.lot === lot)?.values || [];
-										return (
-											<Stack key={lot} gap={4}>
-												<Text>{changeCase(lot)}</Text>
-												<TagsInput
-													placeholder="Enter more providers"
-													value={existingValues}
-													disabled={!!isEditDisabled}
-													onChange={(val) => {
-														if (val) {
-															const newWatchProviders =
-																Array.from(watchProviders);
-															let existingMediaLot = newWatchProviders.find(
-																(wp) => wp.lot === lot,
-															);
-															if (!existingMediaLot) {
-																existingMediaLot = { lot, values: val };
-																newWatchProviders.push(existingMediaLot);
-															} else {
-																existingMediaLot.values = val;
-															}
-															setWatchProviders(newWatchProviders);
-															appendPref(
-																"general.watch_providers",
-																JSON.stringify(newWatchProviders),
-															);
+								{Object.values(MediaLot).map((lot) => {
+									const watchProviders =
+										changingUserPreferences.value.general.watchProviders;
+									const existingValues =
+										watchProviders.find((wp) => wp.lot === lot)?.values || [];
+									return (
+										<Stack key={lot} gap={4}>
+											<Text>{changeCase(lot)}</Text>
+											<TagsInput
+												disabled={!!isEditDisabled}
+												defaultValue={existingValues}
+												placeholder="Enter more providers"
+												onChange={(val) => {
+													if (val) {
+														const newWatchProviders =
+															Array.from(watchProviders);
+														let existingMediaLot = newWatchProviders.find(
+															(wp) => wp.lot === lot,
+														);
+														if (!existingMediaLot) {
+															existingMediaLot = {
+																values: val,
+																lot: lot as MediaLot,
+															};
+															newWatchProviders.push(existingMediaLot);
+														} else {
+															existingMediaLot.values = val;
 														}
-													}}
-												/>
-											</Stack>
-										);
-									})}
+														updatePreference((draft) => {
+															draft.general.watchProviders = newWatchProviders;
+														});
+													}
+												}}
+											/>
+										</Stack>
+									);
+								})}
 							</Stack>
 						</Stack>
 					</Tabs.Panel>
@@ -425,10 +377,9 @@ export default function Page() {
 								defaultChecked={userPreferences.notifications.enabled}
 								disabled={!!isEditDisabled}
 								onChange={(ev) => {
-									appendPref(
-										"notifications.enabled",
-										String(ev.currentTarget.checked),
-									);
+									updatePreference((draft) => {
+										draft.notifications.enabled = ev.currentTarget.checked;
+									});
 								}}
 							/>
 							<Divider />
@@ -439,8 +390,26 @@ export default function Page() {
 							<SimpleGrid cols={{ md: 2 }}>
 								{Object.values(MediaStateChanged).map((name) => (
 									<Switch
-										key={name}
 										size="xs"
+										key={name}
+										styles={{ track: { flex: "none" } }}
+										defaultChecked={userPreferences.notifications.toSend.includes(
+											name,
+										)}
+										disabled={
+											!!isEditDisabled || !userPreferences.notifications.enabled
+										}
+										onChange={() => {
+											const alreadyToSend = new Set(
+												changingUserPreferences.value.notifications.toSend,
+											);
+											const alreadyHas = alreadyToSend.has(name);
+											if (!alreadyHas) alreadyToSend.add(name);
+											else alreadyToSend.delete(name);
+											updatePreference((draft) => {
+												draft.notifications.toSend = Array.from(alreadyToSend);
+											});
+										}}
 										label={match(name)
 											.with(
 												MediaStateChanged.MetadataEpisodeNameChanged,
@@ -486,26 +455,6 @@ export default function Page() {
 												() => "New media is associated with a person",
 											)
 											.exhaustive()}
-										defaultChecked={userPreferences.notifications.toSend.includes(
-											name,
-										)}
-										disabled={
-											!!isEditDisabled || !userPreferences.notifications.enabled
-										}
-										onChange={() => {
-											const alreadyToSend = new Set(
-												userPreferences.notifications.toSend,
-											);
-											const alreadyHas = alreadyToSend.has(name);
-											if (!alreadyHas) alreadyToSend.add(name);
-											else alreadyToSend.delete(name);
-											const val = Array.from(alreadyToSend).map((v) => {
-												const n = camelCase(v.toLowerCase());
-												return n[0].toUpperCase() + n.slice(1);
-											});
-											appendPref("notifications.to_send", JSON.stringify(val));
-										}}
-										styles={{ track: { flex: "none" } }}
 									/>
 								))}
 							</SimpleGrid>
@@ -538,15 +487,20 @@ export default function Page() {
 								</Group>
 								<Select
 									size="xs"
+									disabled={!!isEditDisabled}
 									label="Unit system to use for measurements"
+									defaultValue={userPreferences.fitness.exercises.unitSystem}
 									data={Object.values(UserUnitSystem).map((c) => ({
-										value: c.toLowerCase(),
+										value: c,
 										label: startCase(c.toLowerCase()),
 									}))}
-									defaultValue={userPreferences.fitness.exercises.unitSystem.toLowerCase()}
-									disabled={!!isEditDisabled}
 									onChange={(val) => {
-										if (val) appendPref("fitness.exercises.unit_system", val);
+										if (val) {
+											updatePreference((draft) => {
+												draft.fitness.exercises.unitSystem =
+													val as UserUnitSystem;
+											});
+										}
 									}}
 								/>
 							</SimpleGrid>
@@ -569,10 +523,10 @@ export default function Page() {
 													defaultValue={isNumber(value) ? value : undefined}
 													onChange={(val) => {
 														if (isNumber(val)) {
-															appendPref(
-																`fitness.exercises.set_rest_timers.${snakeCase(name)}`,
-																String(val),
-															);
+															updatePreference((draft) => {
+																draft.fitness.exercises.setRestTimers[name] =
+																	val;
+															});
 														}
 													}}
 												/>
@@ -629,10 +583,10 @@ export default function Page() {
 												});
 												return;
 											}
-											appendPref(
-												`fitness.logging.${snakeCase(option)}`,
-												String(ev.currentTarget.checked),
-											);
+											updatePreference((draft) => {
+												draft.fitness.logging[option] =
+													ev.currentTarget.checked;
+											});
 										}}
 									/>
 								);
@@ -650,16 +604,20 @@ export default function Page() {
 											defaultChecked={isEnabled}
 											disabled={!!isEditDisabled}
 											onChange={(ev) => {
-												appendPref(
-													`fitness.measurements.inbuilt.${snakeCase(name)}`,
-													String(ev.currentTarget.checked),
-												);
+												updatePreference((draft) => {
+													// biome-ignore lint/suspicious/noExplicitAny: too much work to use correct types
+													(draft as any).fitness.measurements.inbuilt[name] =
+														ev.currentTarget.checked;
+												});
 											}}
 										/>
 									))}
 								</SimpleGrid>
 							</Input.Wrapper>
 							<JsonInput
+								autosize
+								formatOnBlur
+								disabled={!!isEditDisabled}
 								label="The custom metrics you want to keep track of"
 								description="The name of the attribute along with the data type. Only decimal data type is supported."
 								defaultValue={JSON.stringify(
@@ -667,11 +625,10 @@ export default function Page() {
 									null,
 									4,
 								)}
-								disabled={!!isEditDisabled}
-								autosize
-								formatOnBlur
 								onChange={(v) => {
-									appendPref("fitness.measurements.custom.dummy", v);
+									updatePreference((draft) => {
+										draft.fitness.measurements.custom = JSON.parse(v);
+									});
 								}}
 							/>
 						</Stack>
@@ -690,10 +647,10 @@ const EDITABLE_NUM_ELEMENTS = [
 const EDITABLE_DEDUPLICATE_MEDIA = [DashboardElementLot.Upcoming];
 
 const EditDashboardElement = (props: {
+	index: number;
 	isEditDisabled: boolean;
 	lot: DashboardElementLot;
-	index: number;
-	appendPref: (property: string, value: string) => void;
+	updatePreference: (makeChange: UpdatePreferenceFunc) => void;
 }) => {
 	const userPreferences = useUserPreferences();
 	const focusedElementIndex = userPreferences.general.dashboard.findIndex(
@@ -742,10 +699,9 @@ const EditDashboardElement = (props: {
 									userPreferences.general.dashboard,
 								);
 								newDashboardData[focusedElementIndex].hidden = newValue;
-								props.appendPref(
-									"general.dashboard",
-									JSON.stringify(newDashboardData),
-								);
+								props.updatePreference((draft) => {
+									draft.general.dashboard = newDashboardData;
+								});
 							}}
 						/>
 					</Group>
@@ -762,10 +718,9 @@ const EditDashboardElement = (props: {
 											userPreferences.general.dashboard,
 										);
 										newDashboardData[focusedElementIndex].numElements = num;
-										props.appendPref(
-											"general.dashboard",
-											JSON.stringify(newDashboardData),
-										);
+										props.updatePreference((draft) => {
+											draft.general.dashboard = newDashboardData;
+										});
 									}
 								}}
 							/>
@@ -785,10 +740,9 @@ const EditDashboardElement = (props: {
 									);
 									newDashboardData[focusedElementIndex].deduplicateMedia =
 										newValue;
-									props.appendPref(
-										"general.dashboard",
-										JSON.stringify(newDashboardData),
-									);
+									props.updatePreference((draft) => {
+										draft.general.dashboard = newDashboardData;
+									});
 								}}
 							/>
 						) : null}
diff --git a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
index 0d10dd7eb3..4b27608566 100644
--- a/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.profile-and-sharing.tsx
@@ -54,8 +54,11 @@ import { match } from "ts-pattern";
 import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
-import { confirmWrapper } from "~/components/confirmation";
-import { applicationBaseUrl, dayjsLib } from "~/lib/generals";
+import {
+	applicationBaseUrl,
+	dayjsLib,
+	openConfirmationModal,
+} from "~/lib/generals";
 import {
 	useConfirmSubmit,
 	useCoreDetails,
@@ -236,14 +239,13 @@ export default function Page() {
 
 									<Button
 										type="submit"
-										onClick={async (e) => {
+										onClick={(e) => {
 											const form = e.currentTarget.form;
 											e.preventDefault();
-											const conf = await confirmWrapper({
-												confirmation:
-													"Are you sure you want to update your profile?",
-											});
-											if (conf && form) submit(form);
+											openConfirmationModal(
+												"Are you sure you want to update your profile?",
+												() => submit(form),
+											);
 										}}
 										fullWidth
 									>
@@ -411,14 +413,13 @@ const DisplayAccessLink = (props: {
 											type="submit"
 											variant="subtle"
 											disabled={props.isEditDisabled || undefined}
-											onClick={async (e) => {
+											onClick={(e) => {
 												const form = e.currentTarget.form;
 												e.preventDefault();
-												const conf = await confirmWrapper({
-													confirmation:
-														"Are you sure you want to revoke this access link?",
-												});
-												if (conf && form) submit(form);
+												openConfirmationModal(
+													"Are you sure you want to revoke this access link?",
+													() => submit(form),
+												);
 											}}
 										>
 											<IconLock />
diff --git a/apps/frontend/app/routes/_dashboard.settings.users.tsx b/apps/frontend/app/routes/_dashboard.settings.users.tsx
index 85e98fc177..6348080510 100644
--- a/apps/frontend/app/routes/_dashboard.settings.users.tsx
+++ b/apps/frontend/app/routes/_dashboard.settings.users.tsx
@@ -54,7 +54,7 @@ import { withQuery } from "ufo";
 import { z } from "zod";
 import { zx } from "zodix";
 import { DebouncedSearchInput } from "~/components/common";
-import { confirmWrapper } from "~/components/confirmation";
+import { openConfirmationModal } from "~/lib/generals";
 import { useConfirmSubmit, useCoreDetails } from "~/lib/hooks";
 import {
 	createToastHeaders,
@@ -304,13 +304,13 @@ const UserDisplay = (props: { index: number }) => {
 							color="red"
 							type="submit"
 							variant="subtle"
-							onClick={async (e) => {
+							onClick={(e) => {
 								const form = e.currentTarget.form;
 								e.preventDefault();
-								const conf = await confirmWrapper({
-									confirmation: "Are you sure you want to delete this user?",
-								});
-								if (conf && form) submit(form);
+								openConfirmationModal(
+									"Are you sure you want to delete this user?",
+									() => submit(form),
+								);
 							}}
 						>
 							<IconTrash />
diff --git a/apps/frontend/package.json b/apps/frontend/package.json
index 1755b5c0e1..6bffa386c6 100644
--- a/apps/frontend/package.json
+++ b/apps/frontend/package.json
@@ -19,6 +19,7 @@
 		"@mantine/dates": "7.14.3",
 		"@mantine/form": "7.14.3",
 		"@mantine/hooks": "7.14.3",
+		"@mantine/modals": "7.14.3",
 		"@mantine/notifications": "7.14.3",
 		"@remix-pwa/sw": "3.0.10",
 		"@remix-pwa/worker-runtime": "2.1.4",
@@ -51,7 +52,6 @@
 		"mantine-datatable": "7.14.5",
 		"nanoid": "5.0.9",
 		"react": "18.3.1",
-		"react-confirm": "0.3.0-7",
 		"react-dom": "18.3.1",
 		"react-virtuoso": "4.12.3",
 		"recharts": "2.14.1",
diff --git a/crates/migrations/src/lib.rs b/crates/migrations/src/lib.rs
index 57d83159d4..bc7435521e 100644
--- a/crates/migrations/src/lib.rs
+++ b/crates/migrations/src/lib.rs
@@ -56,6 +56,7 @@ mod m20241214_create_user_notification;
 mod m20241215_changes_for_issue_1131;
 mod m20241220_changes_for_issue_1141;
 mod m20241220_changes_for_issue_49;
+mod m20241224_changes_for_issue_1139;
 
 pub use m20230410_create_metadata::Metadata as AliasedMetadata;
 pub use m20230413_create_person::Person as AliasedPerson;
@@ -79,9 +80,9 @@ impl MigratorTrait for Migrator {
     fn migrations() -> Vec<Box<dyn MigrationTrait>> {
         vec![
             Box::new(m20230409_create_extensions::Migration),
+            Box::new(m20230417_create_user::Migration),
             Box::new(m20230410_create_metadata::Migration),
             Box::new(m20230413_create_person::Migration),
-            Box::new(m20230417_create_user::Migration),
             Box::new(m20230419_create_seen::Migration),
             Box::new(m20230501_create_metadata_group::Migration),
             Box::new(m20230502_create_genre::Migration),
@@ -133,6 +134,7 @@ impl MigratorTrait for Migrator {
             Box::new(m20241215_changes_for_issue_1131::Migration),
             Box::new(m20241220_changes_for_issue_49::Migration),
             Box::new(m20241220_changes_for_issue_1141::Migration),
+            Box::new(m20241224_changes_for_issue_1139::Migration),
         ]
     }
 }
diff --git a/crates/migrations/src/m20230409_create_extensions.rs b/crates/migrations/src/m20230409_create_extensions.rs
index c9e77f17b3..708718d348 100644
--- a/crates/migrations/src/m20230409_create_extensions.rs
+++ b/crates/migrations/src/m20230409_create_extensions.rs
@@ -1,3 +1,4 @@
+// FIXME: Rename this to m20230403_create_extensions in the next major release
 use sea_orm_migration::prelude::*;
 
 #[derive(DeriveMigrationName)]
diff --git a/crates/migrations/src/m20230410_create_metadata.rs b/crates/migrations/src/m20230410_create_metadata.rs
index 7a3346bd61..ec83f3d6eb 100644
--- a/crates/migrations/src/m20230410_create_metadata.rs
+++ b/crates/migrations/src/m20230410_create_metadata.rs
@@ -1,8 +1,11 @@
 use sea_orm_migration::prelude::*;
 
+use super::m20230417_create_user::User;
+
 #[derive(DeriveMigrationName)]
 pub struct Migration;
 
+pub static METADATA_TO_USER_FOREIGN_KEY: &str = "metadata_to_user_foreign_key";
 pub static METADATA_UNIQUE_INDEX: &str = "metadata-identifier-source-lot__unique-index";
 
 // This is responsible for storing common metadata about all media items
@@ -52,6 +55,7 @@ pub enum Metadata {
     MangaSpecifics,
     MovieSpecifics,
     ShowSpecifics,
+    CreatedByUserId,
     VideoGameSpecifics,
     VisualNovelSpecifics,
     MusicSpecifics,
@@ -108,6 +112,15 @@ impl MigrationTrait for Migration {
                     .col(ColumnDef::new(Metadata::IsRecommendation).boolean())
                     .col(ColumnDef::new(Metadata::ExternalIdentifiers).json_binary())
                     .col(ColumnDef::new(Metadata::MusicSpecifics).json_binary())
+                    .col(ColumnDef::new(Metadata::CreatedByUserId).text())
+                    .foreign_key(
+                        ForeignKey::create()
+                            .name(METADATA_TO_USER_FOREIGN_KEY)
+                            .from(Metadata::Table, Metadata::CreatedByUserId)
+                            .to(User::Table, User::Id)
+                            .on_delete(ForeignKeyAction::SetNull)
+                            .on_update(ForeignKeyAction::Cascade),
+                    )
                     .to_owned(),
             )
             .await?;
diff --git a/crates/migrations/src/m20230417_create_user.rs b/crates/migrations/src/m20230417_create_user.rs
index 501b7ad214..a0f438dc58 100644
--- a/crates/migrations/src/m20230417_create_user.rs
+++ b/crates/migrations/src/m20230417_create_user.rs
@@ -1,3 +1,4 @@
+// FIXME: Rename this to m20230404_create_user in the next major release
 use sea_orm_migration::prelude::*;
 
 #[derive(DeriveMigrationName)]
diff --git a/crates/migrations/src/m20241224_changes_for_issue_1139.rs b/crates/migrations/src/m20241224_changes_for_issue_1139.rs
new file mode 100644
index 0000000000..b2c6d185a7
--- /dev/null
+++ b/crates/migrations/src/m20241224_changes_for_issue_1139.rs
@@ -0,0 +1,30 @@
+use sea_orm_migration::prelude::*;
+
+use crate::m20230410_create_metadata::METADATA_TO_USER_FOREIGN_KEY;
+
+#[derive(DeriveMigrationName)]
+pub struct Migration;
+
+#[async_trait::async_trait]
+impl MigrationTrait for Migration {
+    async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
+        let db = manager.get_connection();
+        if !manager.has_column("metadata", "created_by_user_id").await? {
+            db.execute_unprepared(&format!(
+            r#"
+ALTER TABLE "metadata" ADD COLUMN "created_by_user_id" TEXT;
+ALTER TABLE "metadata" ADD CONSTRAINT "{fk}" FOREIGN KEY ("created_by_user_id") REFERENCES "user"("id") ON DELETE SET NULL ON UPDATE CASCADE;
+        "#,
+        fk = METADATA_TO_USER_FOREIGN_KEY
+        ))
+        .await?;
+            db.execute_unprepared(r#"
+UPDATE "metadata" SET "created_by_user_id" = (SELECT "id" FROM "user" WHERE lot = 'admin') WHERE source = 'custom'"#).await?;
+        }
+        Ok(())
+    }
+
+    async fn down(&self, _manager: &SchemaManager) -> Result<(), DbErr> {
+        Ok(())
+    }
+}
diff --git a/crates/models/common/src/lib.rs b/crates/models/common/src/lib.rs
index 92209c24c6..eed8803f45 100644
--- a/crates/models/common/src/lib.rs
+++ b/crates/models/common/src/lib.rs
@@ -45,13 +45,6 @@ pub enum StoredUrl {
     Url(String),
 }
 
-#[derive(Debug, InputObject)]
-pub struct UpdateComplexJsonInput {
-    pub value: String,
-    /// Dot delimited path to the property that needs to be changed.
-    pub property: String,
-}
-
 #[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Enum, ConfigEnum)]
 pub enum CollectionExtraInformationLot {
     Date,
@@ -326,6 +319,7 @@ pub enum ApplicationCacheKey {
     TmdbSettings,
     ListennotesSettings,
     UserCollectionsList(UserLevelCacheKey<()>),
+    UserRecommendationsKey(UserLevelCacheKey<()>),
     UserAnalyticsParameters(UserLevelCacheKey<()>),
     PeopleSearch(UserLevelCacheKey<PeopleSearchInput>),
     UserAnalytics(UserLevelCacheKey<UserAnalyticsInput>),
diff --git a/crates/models/database/src/metadata.rs b/crates/models/database/src/metadata.rs
index d6eb422953..c2c7a9b77a 100644
--- a/crates/models/database/src/metadata.rs
+++ b/crates/models/database/src/metadata.rs
@@ -18,42 +18,43 @@ use serde::{Deserialize, Serialize};
 pub struct Model {
     #[sea_orm(primary_key, auto_increment = false)]
     pub id: String,
-    pub created_on: DateTimeUtc,
     pub lot: MediaLot,
-    pub last_updated_on: DateTimeUtc,
     pub title: String,
     pub identifier: String,
     pub source: MediaSource,
     pub is_nsfw: Option<bool>,
+    pub created_on: DateTimeUtc,
     pub is_partial: Option<bool>,
+    pub publish_year: Option<i32>,
     pub source_url: Option<String>,
-    pub is_recommendation: Option<bool>,
     pub description: Option<String>,
-    pub original_language: Option<String>,
-    pub publish_year: Option<i32>,
+    pub last_updated_on: DateTimeUtc,
+    pub is_recommendation: Option<bool>,
     pub publish_date: Option<NaiveDate>,
-    pub production_status: Option<String>,
     pub provider_rating: Option<Decimal>,
+    pub original_language: Option<String>,
+    pub production_status: Option<String>,
     #[sea_orm(column_type = "Json")]
     pub images: Option<Vec<MetadataImage>>,
+    pub created_by_user_id: Option<String>,
     #[sea_orm(column_type = "Json")]
     pub videos: Option<Vec<MetadataVideo>>,
-    #[sea_orm(column_type = "Json")]
-    pub free_creators: Option<Vec<MetadataFreeCreator>>,
+    pub book_specifics: Option<BookSpecifics>,
+    pub show_specifics: Option<ShowSpecifics>,
+    pub anime_specifics: Option<AnimeSpecifics>,
+    pub manga_specifics: Option<MangaSpecifics>,
+    pub music_specifics: Option<MusicSpecifics>,
+    pub movie_specifics: Option<MovieSpecifics>,
+    pub podcast_specifics: Option<PodcastSpecifics>,
     #[sea_orm(column_type = "Json")]
     pub watch_providers: Option<Vec<WatchProvider>>,
     #[sea_orm(column_type = "Json")]
-    pub external_identifiers: Option<MetadataExternalIdentifiers>,
+    pub free_creators: Option<Vec<MetadataFreeCreator>>,
     pub audio_book_specifics: Option<AudioBookSpecifics>,
-    pub book_specifics: Option<BookSpecifics>,
-    pub movie_specifics: Option<MovieSpecifics>,
-    pub podcast_specifics: Option<PodcastSpecifics>,
-    pub show_specifics: Option<ShowSpecifics>,
     pub video_game_specifics: Option<VideoGameSpecifics>,
     pub visual_novel_specifics: Option<VisualNovelSpecifics>,
-    pub anime_specifics: Option<AnimeSpecifics>,
-    pub manga_specifics: Option<MangaSpecifics>,
-    pub music_specifics: Option<MusicSpecifics>,
+    #[sea_orm(column_type = "Json")]
+    pub external_identifiers: Option<MetadataExternalIdentifiers>,
 }
 
 #[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
@@ -72,6 +73,14 @@ pub enum Relation {
     Review,
     #[sea_orm(has_many = "super::seen::Entity")]
     Seen,
+    #[sea_orm(
+        belongs_to = "super::user::Entity",
+        from = "Column::CreatedByUserId",
+        to = "super::user::Column::Id",
+        on_update = "Cascade",
+        on_delete = "SetNull"
+    )]
+    User,
     #[sea_orm(has_many = "super::user_to_entity::Entity")]
     UserToEntity,
 }
@@ -118,6 +127,12 @@ impl Related<super::seen::Entity> for Entity {
     }
 }
 
+impl Related<super::user::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::User.def()
+    }
+}
+
 impl Related<super::user_to_entity::Entity> for Entity {
     fn to() -> RelationDef {
         Relation::UserToEntity.def()
diff --git a/crates/models/database/src/user.rs b/crates/models/database/src/user.rs
index 220023c5a3..b527019233 100644
--- a/crates/models/database/src/user.rs
+++ b/crates/models/database/src/user.rs
@@ -45,6 +45,8 @@ pub enum Relation {
     ImportReport,
     #[sea_orm(has_many = "super::integration::Entity")]
     Integration,
+    #[sea_orm(has_many = "super::metadata::Entity")]
+    Metadata,
     #[sea_orm(has_many = "super::notification_platform::Entity")]
     NotificationPlatform,
     #[sea_orm(has_many = "super::review::Entity")]
@@ -99,6 +101,12 @@ impl Related<super::integration::Entity> for Entity {
     }
 }
 
+impl Related<super::metadata::Entity> for Entity {
+    fn to() -> RelationDef {
+        Relation::Metadata.def()
+    }
+}
+
 impl Related<super::notification_platform::Entity> for Entity {
     fn to() -> RelationDef {
         Relation::NotificationPlatform.def()
diff --git a/crates/models/dependent/src/lib.rs b/crates/models/dependent/src/lib.rs
index 0f0fd48078..f358e8d48a 100644
--- a/crates/models/dependent/src/lib.rs
+++ b/crates/models/dependent/src/lib.rs
@@ -408,6 +408,12 @@ pub struct IgdbSettings {
     pub access_token: String,
 }
 
+#[skip_serializing_none]
+#[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone)]
+pub struct UserRecommendationsKey {
+    pub recommendations_key: String,
+}
+
 #[skip_serializing_none]
 #[derive(Debug, Default, PartialEq, Eq, Serialize, Deserialize, Clone)]
 pub struct EmptyCacheValue {
@@ -431,6 +437,7 @@ pub enum ApplicationCacheValue {
     MetadataSearch(MetadataSearchResponse),
     ListennotesSettings(ListennotesSettings),
     UserAnalyticsParameters(ApplicationDateRange),
+    UserRecommendationsKey(UserRecommendationsKey),
     UserCollectionsList(UserCollectionsListResponse),
     MetadataGroupSearch(MetadataGroupSearchResponse),
 }
diff --git a/crates/models/fitness/src/lib.rs b/crates/models/fitness/src/lib.rs
index e4c4b16fa1..da5f3d3391 100644
--- a/crates/models/fitness/src/lib.rs
+++ b/crates/models/fitness/src/lib.rs
@@ -1,7 +1,7 @@
 use std::collections::HashMap;
 
 use async_graphql::{Enum, InputObject, SimpleObject};
-use common_models::{SearchInput, StoredUrl, UpdateComplexJsonInput};
+use common_models::{SearchInput, StoredUrl};
 use derive_more::{Add, AddAssign, Sum};
 use educe::Educe;
 use enum_models::{
@@ -290,8 +290,18 @@ pub struct UserToExerciseBestSetExtraInformation {
 
 #[skip_serializing_none]
 #[derive(
-    Debug, Clone, Serialize, Deserialize, FromJsonQueryResult, Eq, PartialEq, SimpleObject, Educe,
+    Eq,
+    Educe,
+    Debug,
+    Clone,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "SetRestTimersSettingsInput")]
 #[educe(Default)]
 pub struct SetRestTimersSettings {
     pub drop: Option<u16>,
@@ -302,8 +312,18 @@ pub struct SetRestTimersSettings {
 }
 
 #[derive(
-    Debug, Clone, Serialize, Deserialize, FromJsonQueryResult, Eq, PartialEq, SimpleObject, Default,
+    Eq,
+    Debug,
+    Clone,
+    Default,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserToExerciseSettingsExtraInformationInput")]
 pub struct UserToExerciseSettingsExtraInformation {
     pub exclude_from_analytics: bool,
     pub set_rest_timers: SetRestTimersSettings,
@@ -674,7 +694,7 @@ pub struct ExercisesListInput {
 #[derive(Debug, InputObject)]
 pub struct UpdateUserExerciseSettings {
     pub exercise_id: String,
-    pub change: UpdateComplexJsonInput,
+    pub change: UserToExerciseSettingsExtraInformation,
 }
 
 #[derive(Clone, Debug, Deserialize, Serialize, InputObject)]
diff --git a/crates/models/media/src/lib.rs b/crates/models/media/src/lib.rs
index 432bde6bce..9d42f1e642 100644
--- a/crates/models/media/src/lib.rs
+++ b/crates/models/media/src/lib.rs
@@ -1026,6 +1026,12 @@ pub struct CreateCustomMetadataInput {
     pub visual_novel_specifics: Option<VisualNovelSpecifics>,
 }
 
+#[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
+pub struct UpdateCustomMetadataInput {
+    pub existing_metadata_id: String,
+    pub update: CreateCustomMetadataInput,
+}
+
 #[derive(Debug, Serialize, Deserialize, InputObject, Clone)]
 pub struct CreateUserIntegrationInput {
     pub provider: IntegrationProvider,
@@ -1254,6 +1260,7 @@ pub struct GraphqlMetadataDetails {
     pub provider_rating: Option<Decimal>,
     pub original_language: Option<String>,
     pub production_status: Option<String>,
+    pub created_by_user_id: Option<String>,
     pub group: Option<GraphqlMetadataGroup>,
     pub watch_providers: Vec<WatchProvider>,
     pub show_specifics: Option<ShowSpecifics>,
diff --git a/crates/models/user/src/lib.rs b/crates/models/user/src/lib.rs
index c7787e3018..e0b562b90b 100644
--- a/crates/models/user/src/lib.rs
+++ b/crates/models/user/src/lib.rs
@@ -20,9 +20,19 @@ const MOVIE_WATCH_PROVIDERS: [&str; 8] = [
 ];
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Clone,
+    Debug,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserNotificationsPreferencesInput")]
 pub struct UserNotificationsPreferences {
     #[educe(Default(expression = MediaStateChanged::iter().collect()))]
     pub to_send: Vec<MediaStateChanged>,
@@ -31,9 +41,19 @@ pub struct UserNotificationsPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Clone,
+    Debug,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserMediaFeaturesEnabledPreferencesInput")]
 pub struct UserMediaFeaturesEnabledPreferences {
     #[educe(Default = true)]
     pub enabled: bool,
@@ -66,9 +86,19 @@ pub struct UserMediaFeaturesEnabledPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Clone,
+    Debug,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserOthersFeaturesEnabledPreferencesInput")]
 pub struct UserOthersFeaturesEnabledPreferences {
     #[educe(Default = true)]
     pub collections: bool,
@@ -77,9 +107,19 @@ pub struct UserOthersFeaturesEnabledPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Clone,
+    Educe,
+    Debug,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserFitnessFeaturesEnabledPreferencesInput")]
 pub struct UserFitnessFeaturesEnabledPreferences {
     #[educe(Default = true)]
     pub enabled: bool,
@@ -92,8 +132,18 @@ pub struct UserFitnessFeaturesEnabledPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Clone,
+    Debug,
+    Educe,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserFitnessLoggingPreferencesInput")]
 #[educe(Default)]
 pub struct UserFitnessLoggingPreferences {
     pub mute_sounds: bool,
@@ -103,9 +153,19 @@ pub struct UserFitnessLoggingPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Debug,
+    Clone,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserFitnessExercisesPreferencesInput")]
 pub struct UserFitnessExercisesPreferences {
     #[educe(Default = UserUnitSystem::Metric)]
     pub unit_system: UserUnitSystem,
@@ -113,8 +173,18 @@ pub struct UserFitnessExercisesPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Debug,
+    Clone,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserMeasurementsInBuiltPreferencesInput")]
 #[educe(Default)]
 pub struct UserMeasurementsInBuiltPreferences {
     #[educe(Default = true)]
@@ -161,8 +231,18 @@ pub enum UserCustomMeasurementDataType {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Default,
+    Eq,
+    Clone,
+    Debug,
+    Default,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserCustomMeasurementInput")]
 #[serde(rename_all = "camelCase")]
 pub struct UserCustomMeasurement {
     pub name: String,
@@ -170,8 +250,18 @@ pub struct UserCustomMeasurement {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Debug,
+    Clone,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserFitnessMeasurementsPreferencesInput")]
 #[educe(Default)]
 pub struct UserFitnessMeasurementsPreferences {
     #[educe(Default(expression = vec![UserCustomMeasurement {
@@ -184,8 +274,18 @@ pub struct UserFitnessMeasurementsPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Clone,
+    Debug,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserAnalyticsFeaturesEnabledPreferencesInput")]
 #[educe(Default)]
 pub struct UserAnalyticsFeaturesEnabledPreferences {
     #[educe(Default = true)]
@@ -193,8 +293,18 @@ pub struct UserAnalyticsFeaturesEnabledPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, Default, FromJsonQueryResult,
+    Eq,
+    Debug,
+    Clone,
+    Default,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserFeaturesEnabledPreferencesInput")]
 pub struct UserFeaturesEnabledPreferences {
     pub media: UserMediaFeaturesEnabledPreferences,
     pub others: UserOthersFeaturesEnabledPreferences,
@@ -203,8 +313,18 @@ pub struct UserFeaturesEnabledPreferences {
 }
 
 #[derive(
-    Eq, Clone, Debug, Educe, Serialize, PartialEq, Deserialize, SimpleObject, FromJsonQueryResult,
+    Eq,
+    Clone,
+    Debug,
+    Educe,
+    Serialize,
+    PartialEq,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserFitnessPreferencesInput")]
 #[educe(Default)]
 pub struct UserFitnessPreferences {
     pub logging: UserFitnessLoggingPreferences,
@@ -267,9 +387,19 @@ pub enum DashboardElementLot {
 
 #[skip_serializing_none]
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Default,
+    Eq,
+    Clone,
+    Debug,
+    Default,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[serde(rename_all = "camelCase")]
+#[graphql(input_name = "UserGeneralDashboardElementInput")]
 pub struct UserGeneralDashboardElement {
     pub hidden: bool,
     pub num_elements: Option<u64>,
@@ -279,18 +409,37 @@ pub struct UserGeneralDashboardElement {
 
 #[skip_serializing_none]
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult,
+    Eq,
+    Clone,
+    Debug,
+    Serialize,
+    PartialEq,
+    InputObject,
+    Deserialize,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[serde(rename_all = "camelCase")]
+#[graphql(input_name = "UserGeneralWatchProviderInput")]
 pub struct UserGeneralWatchProvider {
     pub lot: MediaLot,
     pub values: Vec<String>,
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, FromJsonQueryResult, Educe,
+    Eq,
+    Educe,
+    Debug,
+    Clone,
+    PartialEq,
+    Serialize,
+    Deserialize,
+    InputObject,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
 #[educe(Default)]
+#[graphql(input_name = "UserGeneralPreferencesInput")]
 pub struct UserGeneralPreferences {
     #[educe(Default = true)]
     pub display_nsfw: bool,
@@ -344,8 +493,18 @@ pub struct UserGeneralPreferences {
 }
 
 #[derive(
-    Debug, Serialize, Deserialize, SimpleObject, Clone, Eq, PartialEq, Default, FromJsonQueryResult,
+    Eq,
+    Clone,
+    Debug,
+    Default,
+    PartialEq,
+    Serialize,
+    InputObject,
+    Deserialize,
+    SimpleObject,
+    FromJsonQueryResult,
 )]
+#[graphql(input_name = "UserPreferencesInput")]
 pub struct UserPreferences {
     pub fitness: UserFitnessPreferences,
     pub general: UserGeneralPreferences,
diff --git a/crates/resolvers/miscellaneous/src/lib.rs b/crates/resolvers/miscellaneous/src/lib.rs
index 53a5892a16..eb91dd741f 100644
--- a/crates/resolvers/miscellaneous/src/lib.rs
+++ b/crates/resolvers/miscellaneous/src/lib.rs
@@ -14,8 +14,8 @@ use media_models::{
     CommitMediaInput, CommitPersonInput, CreateCustomMetadataInput, CreateOrUpdateReviewInput,
     CreateReviewCommentInput, GenreDetailsInput, GenreListItem, GraphqlCalendarEvent,
     GraphqlMetadataDetails, GroupedCalendarEvent, MetadataGroupsListInput, MetadataListInput,
-    MetadataPartialDetails, PeopleListInput, ProgressUpdateInput, UpdateSeenItemInput,
-    UserCalendarEventInput, UserUpcomingCalendarEventInput,
+    MetadataPartialDetails, PeopleListInput, ProgressUpdateInput, UpdateCustomMetadataInput,
+    UpdateSeenItemInput, UserCalendarEventInput, UserUpcomingCalendarEventInput,
 };
 use miscellaneous_service::MiscellaneousService;
 use traits::AuthProvider;
@@ -271,6 +271,17 @@ impl MiscellaneousMutation {
             .map(|m| StringIdObject { id: m.id })
     }
 
+    /// Update custom metadata.
+    async fn update_custom_metadata(
+        &self,
+        gql_ctx: &Context<'_>,
+        input: UpdateCustomMetadataInput,
+    ) -> Result<bool> {
+        let service = gql_ctx.data_unchecked::<Arc<MiscellaneousService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.update_custom_metadata(&user_id, input).await
+    }
+
     /// Deploy job to update progress of media items in bulk. For seen items in progress,
     /// progress is updated only if it has actually changed.
     async fn deploy_bulk_progress_update(
diff --git a/crates/resolvers/user/src/lib.rs b/crates/resolvers/user/src/lib.rs
index baddc93fb8..6bd927502e 100644
--- a/crates/resolvers/user/src/lib.rs
+++ b/crates/resolvers/user/src/lib.rs
@@ -1,7 +1,7 @@
 use std::sync::Arc;
 
 use async_graphql::{Context, Object, Result};
-use common_models::{StringIdObject, UpdateComplexJsonInput};
+use common_models::StringIdObject;
 use database_models::{access_link, integration, notification_platform, user};
 use dependent_models::UserDetailsResult;
 use media_models::{
@@ -11,7 +11,7 @@ use media_models::{
     UpdateUserNotificationPlatformInput,
 };
 use traits::AuthProvider;
-use user_models::UpdateUserInput;
+use user_models::{UpdateUserInput, UserPreferences};
 use user_service::UserService;
 
 #[derive(Default)]
@@ -174,7 +174,7 @@ impl UserMutation {
     async fn update_user_preference(
         &self,
         gql_ctx: &Context<'_>,
-        input: UpdateComplexJsonInput,
+        input: UserPreferences,
     ) -> Result<bool> {
         let service = gql_ctx.data_unchecked::<Arc<UserService>>();
         let user_id = self.user_id_from_ctx(gql_ctx).await?;
@@ -268,4 +268,11 @@ impl UserMutation {
         let user_id = self.user_id_from_ctx(gql_ctx).await?;
         service.generate_auth_token(user_id).await
     }
+
+    /// Refresh the user recommendations key.
+    async fn refresh_user_recommendations_key(&self, gql_ctx: &Context<'_>) -> Result<bool> {
+        let service = gql_ctx.data_unchecked::<Arc<UserService>>();
+        let user_id = self.user_id_from_ctx(gql_ctx).await?;
+        service.refresh_user_recommendations_key(&user_id).await
+    }
 }
diff --git a/crates/services/cache/src/lib.rs b/crates/services/cache/src/lib.rs
index c1d34275d7..082266bfa8 100644
--- a/crates/services/cache/src/lib.rs
+++ b/crates/services/cache/src/lib.rs
@@ -37,6 +37,7 @@ impl CacheService {
             | ApplicationCacheKey::PeopleSearch { .. }
             | ApplicationCacheKey::MetadataSearch { .. }
             | ApplicationCacheKey::MetadataGroupSearch { .. }
+            | ApplicationCacheKey::UserRecommendationsKey { .. }
             | ApplicationCacheKey::MetadataRecentlyConsumed { .. } => 1,
 
             ApplicationCacheKey::UserAnalytics { .. } => 2,
diff --git a/crates/services/fitness/Cargo.toml b/crates/services/fitness/Cargo.toml
index baf62f1e70..6c7492a596 100644
--- a/crates/services/fitness/Cargo.toml
+++ b/crates/services/fitness/Cargo.toml
@@ -16,6 +16,7 @@ dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
 enum-models = { path = "../../models/enum" }
 fitness-models = { path = "../../models/fitness" }
+futures = { workspace = true }
 migrations = { path = "../../migrations" }
 nanoid = { workspace = true }
 reqwest = { workspace = true }
diff --git a/crates/services/fitness/src/lib.rs b/crates/services/fitness/src/lib.rs
index 9b295a0726..529b5ae221 100644
--- a/crates/services/fitness/src/lib.rs
+++ b/crates/services/fitness/src/lib.rs
@@ -32,6 +32,7 @@ use fitness_models::{
     UpdateUserWorkoutAttributesInput, UserMeasurementsListInput, UserToExerciseExtraInformation,
     UserWorkoutInput, WorkoutInformation, WorkoutSetRecord, WorkoutSummary, WorkoutSummaryExercise,
 };
+use futures::TryStreamExt;
 use migrations::AliasedExercise;
 use nanoid::nanoid;
 use sea_orm::{
@@ -602,11 +603,20 @@ impl FitnessService {
     }
 
     pub async fn revise_user_workouts(&self, user_id: String) -> Result<()> {
-        UserToEntity::delete_many()
+        let mut all_stream = UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(&user_id))
             .filter(user_to_entity::Column::ExerciseId.is_not_null())
-            .exec(&self.0.db)
+            .stream(&self.0.db)
             .await?;
+        while let Some(ute) = all_stream.try_next().await? {
+            let mut new = UserToExerciseExtraInformation::default();
+            let eei = ute.exercise_extra_information.clone().unwrap_or_default();
+            new.settings = eei.settings;
+            let mut ute: user_to_entity::ActiveModel = ute.into();
+            ute.exercise_num_times_interacted = ActiveValue::Set(None);
+            ute.exercise_extra_information = ActiveValue::Set(Some(new));
+            ute.update(&self.0.db).await?;
+        }
         let workouts = Workout::find()
             .filter(workout::Column::UserId.eq(&user_id))
             .order_by_asc(workout::Column::EndTime)
@@ -700,7 +710,6 @@ impl FitnessService {
         user_id: String,
         input: UpdateUserExerciseSettings,
     ) -> Result<bool> {
-        let err = || Error::new("Incorrect property value encountered");
         let ute = match UserToEntity::find()
             .filter(user_to_entity::Column::UserId.eq(&user_id))
             .filter(user_to_entity::Column::ExerciseId.eq(&input.exercise_id))
@@ -721,31 +730,7 @@ impl FitnessService {
             }
         };
         let mut exercise_extra_information = ute.clone().exercise_extra_information.unwrap();
-        if input.change.property.contains('.') {
-            let (left, right) = input.change.property.split_once('.').ok_or_else(err)?;
-            match left {
-                "set_rest_timers" => {
-                    let value = input.change.value.parse().unwrap();
-                    let set_rest_timers = &mut exercise_extra_information.settings.set_rest_timers;
-                    match right {
-                        "drop" => set_rest_timers.drop = Some(value),
-                        "normal" => set_rest_timers.normal = Some(value),
-                        "warmup" => set_rest_timers.warmup = Some(value),
-                        "failure" => set_rest_timers.failure = Some(value),
-                        _ => return Err(err()),
-                    }
-                }
-                _ => return Err(err()),
-            };
-        } else {
-            match input.change.property.as_str() {
-                "exclude_from_analytics" => {
-                    exercise_extra_information.settings.exclude_from_analytics =
-                        input.change.value.parse().unwrap();
-                }
-                _ => return Err(err()),
-            }
-        }
+        exercise_extra_information.settings = input.change;
         let mut ute: user_to_entity::ActiveModel = ute.into();
         ute.exercise_extra_information = ActiveValue::Set(Some(exercise_extra_information));
         ute.update(&self.0.db).await?;
diff --git a/crates/services/importer/src/anilist.rs b/crates/services/importer/src/anilist.rs
index 92bf2d3cae..5d07c3e984 100644
--- a/crates/services/importer/src/anilist.rs
+++ b/crates/services/importer/src/anilist.rs
@@ -100,7 +100,7 @@ pub async fn import(
                     history.anime_episode_number = Some(num.try_into().unwrap());
                 }
                 MediaLot::Manga => {
-                    history.manga_chapter_number = Some(num.try_into().unwrap());
+                    history.manga_chapter_number = Some(num.into());
                 }
                 _ => unreachable!(),
             }
diff --git a/crates/services/integration/src/yank/komga.rs b/crates/services/integration/src/yank/komga.rs
index 464b4ca080..b509dd2250 100644
--- a/crates/services/integration/src/yank/komga.rs
+++ b/crates/services/integration/src/yank/komga.rs
@@ -7,7 +7,7 @@ use anyhow::{anyhow, bail, Context, Result};
 use application_utils::get_base_http_client;
 use async_graphql::futures_util::{stream, StreamExt};
 use common_models::DefaultCollection;
-use common_utils::ryot_log;
+use common_utils::{ryot_log, sleep_for_n_seconds};
 use database_models::{metadata, prelude::Metadata};
 use dependent_models::{ImportCompletedItem, ImportResult};
 use enum_models::{MediaLot, MediaSource};
@@ -240,7 +240,7 @@ async fn sse_listener(
         }
 
         ryot_log!(trace, "SSE listener finished");
-        tokio::time::sleep(tokio::time::Duration::from_secs(30)).await;
+        sleep_for_n_seconds(30).await;
     }
 }
 
diff --git a/crates/services/miscellaneous/Cargo.toml b/crates/services/miscellaneous/Cargo.toml
index 33da72f4e0..ce280e100d 100644
--- a/crates/services/miscellaneous/Cargo.toml
+++ b/crates/services/miscellaneous/Cargo.toml
@@ -30,7 +30,6 @@ sea-query = { workspace = true }
 serde = { workspace = true }
 serde_json = { workspace = true }
 supporting-service = { path = "../supporting" }
-tokio = { workspace = true }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
 user-models = { path = "../../models/user" }
diff --git a/crates/services/miscellaneous/src/lib.rs b/crates/services/miscellaneous/src/lib.rs
index 3728bd2a43..152b629e82 100644
--- a/crates/services/miscellaneous/src/lib.rs
+++ b/crates/services/miscellaneous/src/lib.rs
@@ -73,9 +73,10 @@ use media_models::{
     PartialMetadataWithoutId, PeopleListInput, PersonAndMetadataGroupsSortBy,
     PersonDetailsGroupedByRole, PersonDetailsItemWithCharacter, PodcastSpecifics,
     ProgressUpdateInput, ReviewPostedEvent, SeenAnimeExtraInformation, SeenPodcastExtraInformation,
-    SeenShowExtraInformation, ShowSpecifics, UniqueMediaIdentifier, UpdateSeenItemInput,
-    UserCalendarEventInput, UserMediaNextEntry, UserMetadataDetailsEpisodeProgress,
-    UserMetadataDetailsShowSeasonProgress, UserUpcomingCalendarEventInput,
+    SeenShowExtraInformation, ShowSpecifics, UniqueMediaIdentifier, UpdateCustomMetadataInput,
+    UpdateSeenItemInput, UserCalendarEventInput, UserMediaNextEntry,
+    UserMetadataDetailsEpisodeProgress, UserMetadataDetailsShowSeasonProgress,
+    UserUpcomingCalendarEventInput,
 };
 use migrations::{
     AliasedCalendarEvent, AliasedMetadata, AliasedMetadataToGenre, AliasedReview, AliasedSeen,
@@ -103,7 +104,6 @@ use sea_query::{
 };
 use serde::{Deserialize, Serialize};
 use supporting_service::SupportingService;
-use tokio::time::{sleep, Duration as TokioDuration};
 use traits::{MediaProvider, TraceOk};
 use user_models::{DashboardElementLot, UserReviewScale};
 use uuid::Uuid;
@@ -444,6 +444,7 @@ ORDER BY RANDOM() LIMIT 10;
             production_status: model.production_status,
             original_language: model.original_language,
             podcast_specifics: model.podcast_specifics,
+            created_by_user_id: model.created_by_user_id,
             external_identifiers: model.external_identifiers,
             video_game_specifics: model.video_game_specifics,
             audio_book_specifics: model.audio_book_specifics,
@@ -1665,12 +1666,12 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(())
     }
 
-    pub async fn create_custom_metadata(
+    fn get_data_for_custom_metadata(
         &self,
-        user_id: String,
         input: CreateCustomMetadataInput,
-    ) -> Result<metadata::Model> {
-        let identifier = nanoid!(10);
+        identifier: String,
+        user_id: &str,
+    ) -> metadata::ActiveModel {
         let images = input
             .images
             .unwrap_or_default()
@@ -1710,7 +1711,7 @@ ORDER BY RANDOM() LIMIT 10;
             MediaLot::VideoGame => input.video_game_specifics.is_none(),
             MediaLot::VisualNovel => input.visual_novel_specifics.is_none(),
         };
-        let metadata = metadata::ActiveModel {
+        metadata::ActiveModel {
             lot: ActiveValue::Set(input.lot),
             title: ActiveValue::Set(input.title),
             identifier: ActiveValue::Set(identifier),
@@ -1726,6 +1727,7 @@ ORDER BY RANDOM() LIMIT 10;
             movie_specifics: ActiveValue::Set(input.movie_specifics),
             music_specifics: ActiveValue::Set(input.music_specifics),
             podcast_specifics: ActiveValue::Set(input.podcast_specifics),
+            created_by_user_id: ActiveValue::Set(Some(user_id.to_owned())),
             audio_book_specifics: ActiveValue::Set(input.audio_book_specifics),
             video_game_specifics: ActiveValue::Set(input.video_game_specifics),
             visual_novel_specifics: ActiveValue::Set(input.visual_novel_specifics),
@@ -1742,7 +1744,16 @@ ORDER BY RANDOM() LIMIT 10;
                 false => Some(free_creators),
             }),
             ..Default::default()
-        };
+        }
+    }
+
+    pub async fn create_custom_metadata(
+        &self,
+        user_id: String,
+        input: CreateCustomMetadataInput,
+    ) -> Result<metadata::Model> {
+        let identifier = nanoid!(10);
+        let metadata = self.get_data_for_custom_metadata(input.clone(), identifier, &user_id);
         let metadata = metadata.insert(&self.0.db).await?;
         change_metadata_associations(
             &metadata.id,
@@ -1768,6 +1779,53 @@ ORDER BY RANDOM() LIMIT 10;
         Ok(metadata)
     }
 
+    pub async fn update_custom_metadata(
+        &self,
+        user_id: &str,
+        input: UpdateCustomMetadataInput,
+    ) -> Result<bool> {
+        let metadata = Metadata::find_by_id(&input.existing_metadata_id)
+            .one(&self.0.db)
+            .await?
+            .unwrap();
+        if metadata.source != MediaSource::Custom {
+            return Err(Error::new(
+                "This metadata is not custom and cannot be updated",
+            ));
+        }
+        if metadata.created_by_user_id != Some(user_id.to_owned()) {
+            return Err(Error::new("You are not authorized to update this metadata"));
+        }
+        MetadataToGenre::delete_many()
+            .filter(metadata_to_genre::Column::MetadataId.eq(&input.existing_metadata_id))
+            .exec(&self.0.db)
+            .await?;
+        for image in metadata.images.clone().unwrap_or_default() {
+            if let StoredUrl::S3(key) = image.url {
+                self.0.file_storage_service.delete_object(key).await;
+            }
+        }
+        for video in metadata.videos.clone().unwrap_or_default() {
+            if let StoredUrl::S3(key) = video.identifier {
+                self.0.file_storage_service.delete_object(key).await;
+            }
+        }
+        let mut new_metadata =
+            self.get_data_for_custom_metadata(input.update.clone(), metadata.identifier, user_id);
+        new_metadata.id = ActiveValue::Unchanged(input.existing_metadata_id);
+        let metadata = new_metadata.update(&self.0.db).await?;
+        change_metadata_associations(
+            &metadata.id,
+            input.update.genres.unwrap_or_default(),
+            vec![],
+            vec![],
+            vec![],
+            &self.0,
+        )
+        .await?;
+        Ok(true)
+    }
+
     fn get_db_stmt(&self, stmt: SelectStatement) -> Statement {
         let (sql, values) = stmt.build(PostgresQueryBuilder {});
         Statement::from_sql_and_values(DatabaseBackend::Postgres, sql, values)
@@ -2914,7 +2972,6 @@ ORDER BY RANDOM() LIMIT 10;
 
     #[cfg(debug_assertions)]
     pub async fn development_mutation(&self) -> Result<bool> {
-        sleep(TokioDuration::from_secs(3)).await;
         Ok(true)
     }
 }
diff --git a/crates/services/user/Cargo.toml b/crates/services/user/Cargo.toml
index 57c7d44656..d2347a6521 100644
--- a/crates/services/user/Cargo.toml
+++ b/crates/services/user/Cargo.toml
@@ -16,14 +16,12 @@ dependent-models = { path = "../../models/dependent" }
 dependent-utils = { path = "../../utils/dependent" }
 enum_meta = { workspace = true }
 enum-models = { path = "../../models/enum" }
-fitness-models = { path = "../../models/fitness" }
 itertools = { workspace = true }
 jwt-service = { path = "../../services/jwt" }
 media-models = { path = "../../models/media" }
 nanoid = { workspace = true }
 notification-service = { path = "../../services/notification" }
 openidconnect = { workspace = true }
-serde_json = { workspace = true }
 sea-orm = { workspace = true }
 supporting-service = { path = "../supporting" }
 tracing = { workspace = true }
diff --git a/crates/services/user/src/lib.rs b/crates/services/user/src/lib.rs
index df2de4e238..698b41cb71 100644
--- a/crates/services/user/src/lib.rs
+++ b/crates/services/user/src/lib.rs
@@ -1,10 +1,10 @@
-use std::{str::FromStr, sync::Arc};
+use std::sync::Arc;
 
 use application_utils::user_id_from_token;
 use argon2::{Argon2, PasswordHash, PasswordVerifier};
 use async_graphql::{Error, Result};
 use chrono::{Timelike, Utc};
-use common_models::{DefaultCollection, StringIdObject, UpdateComplexJsonInput};
+use common_models::{ApplicationCacheKey, DefaultCollection, StringIdObject, UserLevelCacheKey};
 use common_utils::ryot_log;
 use database_models::{
     access_link, integration, metadata, notification_platform,
@@ -15,11 +15,10 @@ use database_utils::{
     admin_account_guard, deploy_job_to_calculate_user_activities_and_summary, ilike_sql,
     revoke_access_link, server_key_validation_guard, user_by_id,
 };
-use dependent_models::UserDetailsResult;
+use dependent_models::{ApplicationCacheValue, UserDetailsResult, UserRecommendationsKey};
 use dependent_utils::create_or_update_collection;
 use enum_meta::Meta;
 use enum_models::{IntegrationLot, IntegrationProvider, NotificationPlatformLot, UserLot};
-use fitness_models::UserUnitSystem;
 use itertools::Itertools;
 use jwt_service::{sign, AccessLinkClaims};
 use media_models::{
@@ -45,8 +44,7 @@ use sea_orm::{
 };
 use supporting_service::SupportingService;
 use user_models::{
-    DashboardElementLot, GridPacking, NotificationPlatformSpecifics, UpdateUserInput,
-    UserPreferences, UserReviewScale,
+    DashboardElementLot, NotificationPlatformSpecifics, UpdateUserInput, UserPreferences,
 };
 
 fn empty_nonce_verifier(_nonce: Option<&Nonce>) -> Result<(), String> {
@@ -65,14 +63,27 @@ impl UserService {
             .find(|d| d.section == DashboardElementLot::Recommendations)
             .unwrap()
             .num_elements;
-        let current_hour = Utc::now().hour();
+        let recommendation_key = match self
+            .0
+            .cache_service
+            .get_value::<UserRecommendationsKey>(ApplicationCacheKey::UserRecommendationsKey(
+                UserLevelCacheKey {
+                    input: (),
+                    user_id: user_id.to_owned(),
+                },
+            ))
+            .await
+        {
+            Some(k) => k.recommendations_key,
+            None => Utc::now().hour().to_string(),
+        };
         let recs = Metadata::find()
             .filter(metadata::Column::IsRecommendation.eq(true))
             .order_by(
                 Expr::expr(Func::md5(
                     Expr::col(metadata::Column::Title)
                         .concat(Expr::val(user_id))
-                        .concat(Expr::val(current_hour)),
+                        .concat(Expr::val(recommendation_key)),
                 )),
                 Order::Desc,
             )
@@ -85,6 +96,23 @@ impl UserService {
         Ok(recs)
     }
 
+    pub async fn refresh_user_recommendations_key(&self, user_id: &String) -> Result<bool> {
+        let key = nanoid!(12);
+        self.0
+            .cache_service
+            .set_key(
+                ApplicationCacheKey::UserRecommendationsKey(UserLevelCacheKey {
+                    input: (),
+                    user_id: user_id.to_owned(),
+                }),
+                ApplicationCacheValue::UserRecommendationsKey(UserRecommendationsKey {
+                    recommendations_key: key,
+                }),
+            )
+            .await?;
+        Ok(true)
+    }
+
     pub async fn user_access_links(&self, user_id: &String) -> Result<Vec<access_link::Model>> {
         let links = AccessLink::find()
             .filter(access_link::Column::UserId.eq(user_id))
@@ -393,379 +421,11 @@ impl UserService {
     pub async fn update_user_preference(
         &self,
         user_id: String,
-        input: UpdateComplexJsonInput,
+        input: UserPreferences,
     ) -> Result<bool> {
-        let err = || Error::new("Incorrect property value encountered");
         let user_model = user_by_id(&user_id, &self.0).await?;
-        let mut preferences = user_model.preferences.clone();
-        match input.property.is_empty() {
-            true => preferences = UserPreferences::default(),
-            false => {
-                let (left, right) = input.property.split_once('.').ok_or_else(err)?;
-                let value_bool = input.value.parse::<bool>();
-                match left {
-                    "fitness" => {
-                        let (left, right) = right.split_once('.').ok_or_else(err)?;
-                        match left {
-                            "measurements" => {
-                                let (left, right) = right.split_once('.').ok_or_else(err)?;
-                                match left {
-                                    "custom" => {
-                                        preferences.fitness.measurements.custom =
-                                            serde_json::from_str(&input.value).unwrap()
-                                    }
-                                    "inbuilt" => match right {
-                                        "weight" => {
-                                            preferences.fitness.measurements.inbuilt.weight =
-                                                value_bool.unwrap()
-                                        }
-                                        "body_mass_index" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .body_mass_index = value_bool.unwrap();
-                                        }
-                                        "total_body_water" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .total_body_water = value_bool.unwrap();
-                                        }
-                                        "muscle" => {
-                                            preferences.fitness.measurements.inbuilt.muscle =
-                                                value_bool.unwrap();
-                                        }
-                                        "lean_body_mass" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .lean_body_mass = value_bool.unwrap();
-                                        }
-                                        "body_fat" => {
-                                            preferences.fitness.measurements.inbuilt.body_fat =
-                                                value_bool.unwrap();
-                                        }
-                                        "bone_mass" => {
-                                            preferences.fitness.measurements.inbuilt.bone_mass =
-                                                value_bool.unwrap();
-                                        }
-                                        "visceral_fat" => {
-                                            preferences.fitness.measurements.inbuilt.visceral_fat =
-                                                value_bool.unwrap();
-                                        }
-                                        "waist_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .waist_circumference = value_bool.unwrap();
-                                        }
-                                        "waist_to_height_ratio" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .waist_to_height_ratio = value_bool.unwrap();
-                                        }
-                                        "hip_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .hip_circumference = value_bool.unwrap();
-                                        }
-                                        "waist_to_hip_ratio" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .waist_to_hip_ratio = value_bool.unwrap();
-                                        }
-                                        "chest_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .chest_circumference = value_bool.unwrap();
-                                        }
-                                        "thigh_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .thigh_circumference = value_bool.unwrap();
-                                        }
-                                        "biceps_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .biceps_circumference = value_bool.unwrap();
-                                        }
-                                        "neck_circumference" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .neck_circumference = value_bool.unwrap();
-                                        }
-                                        "body_fat_caliper" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .body_fat_caliper = value_bool.unwrap();
-                                        }
-                                        "chest_skinfold" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .chest_skinfold = value_bool.unwrap();
-                                        }
-                                        "abdominal_skinfold" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .abdominal_skinfold = value_bool.unwrap();
-                                        }
-                                        "thigh_skinfold" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .thigh_skinfold = value_bool.unwrap();
-                                        }
-                                        "basal_metabolic_rate" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .basal_metabolic_rate = value_bool.unwrap();
-                                        }
-                                        "total_daily_energy_expenditure" => {
-                                            preferences
-                                                .fitness
-                                                .measurements
-                                                .inbuilt
-                                                .total_daily_energy_expenditure =
-                                                value_bool.unwrap();
-                                        }
-                                        "calories" => {
-                                            preferences.fitness.measurements.inbuilt.calories =
-                                                value_bool.unwrap();
-                                        }
-                                        _ => return Err(err()),
-                                    },
-                                    _ => return Err(err()),
-                                }
-                            }
-                            "exercises" => match right.split_once('.') {
-                                Some((left, right)) => match left {
-                                    "set_rest_timers" => {
-                                        let value = input.value.parse().ok();
-                                        let set_rest_timers =
-                                            &mut preferences.fitness.exercises.set_rest_timers;
-                                        match right {
-                                            "drop" => set_rest_timers.drop = value,
-                                            "normal" => set_rest_timers.normal = value,
-                                            "warmup" => set_rest_timers.warmup = value,
-                                            "failure" => set_rest_timers.failure = value,
-                                            _ => return Err(err()),
-                                        }
-                                    }
-                                    _ => return Err(err()),
-                                },
-                                None => match right {
-                                    "unit_system" => {
-                                        preferences.fitness.exercises.unit_system =
-                                            UserUnitSystem::from_str(&input.value).unwrap();
-                                    }
-                                    _ => return Err(err()),
-                                },
-                            },
-                            "logging" => match right {
-                                "mute_sounds" => {
-                                    preferences.fitness.logging.mute_sounds = value_bool.unwrap();
-                                }
-                                "prompt_for_rest_timer" => {
-                                    preferences.fitness.logging.prompt_for_rest_timer =
-                                        value_bool.unwrap();
-                                }
-                                "show_details_while_editing" => {
-                                    preferences.fitness.logging.show_details_while_editing =
-                                        value_bool.unwrap();
-                                }
-                                _ => return Err(err()),
-                            },
-                            _ => return Err(err()),
-                        }
-                    }
-                    "features_enabled" => {
-                        let (left, right) = right.split_once('.').ok_or_else(err)?;
-                        match left {
-                            "others" => match right {
-                                "collections" => {
-                                    preferences.features_enabled.others.collections =
-                                        value_bool.unwrap()
-                                }
-                                "calendar" => {
-                                    preferences.features_enabled.others.calendar =
-                                        value_bool.unwrap()
-                                }
-                                _ => return Err(err()),
-                            },
-                            "fitness" => match right {
-                                "enabled" => {
-                                    preferences.features_enabled.fitness.enabled =
-                                        value_bool.unwrap()
-                                }
-                                "measurements" => {
-                                    preferences.features_enabled.fitness.measurements =
-                                        value_bool.unwrap()
-                                }
-                                "workouts" => {
-                                    preferences.features_enabled.fitness.workouts =
-                                        value_bool.unwrap()
-                                }
-                                "templates" => {
-                                    preferences.features_enabled.fitness.templates =
-                                        value_bool.unwrap()
-                                }
-
-                                _ => return Err(err()),
-                            },
-                            "analytics" => match right {
-                                "enabled" => {
-                                    preferences.features_enabled.analytics.enabled =
-                                        value_bool.unwrap()
-                                }
-                                _ => return Err(err()),
-                            },
-                            "media" => {
-                                match right {
-                                    "enabled" => {
-                                        preferences.features_enabled.media.enabled =
-                                            value_bool.unwrap()
-                                    }
-                                    "audio_book" => {
-                                        preferences.features_enabled.media.audio_book =
-                                            value_bool.unwrap()
-                                    }
-                                    "book" => {
-                                        preferences.features_enabled.media.book =
-                                            value_bool.unwrap()
-                                    }
-                                    "movie" => {
-                                        preferences.features_enabled.media.movie =
-                                            value_bool.unwrap()
-                                    }
-                                    "music" => {
-                                        preferences.features_enabled.media.music =
-                                            value_bool.unwrap()
-                                    }
-                                    "podcast" => {
-                                        preferences.features_enabled.media.podcast =
-                                            value_bool.unwrap()
-                                    }
-                                    "show" => {
-                                        preferences.features_enabled.media.show =
-                                            value_bool.unwrap()
-                                    }
-                                    "video_game" => {
-                                        preferences.features_enabled.media.video_game =
-                                            value_bool.unwrap()
-                                    }
-                                    "visual_novel" => {
-                                        preferences.features_enabled.media.visual_novel =
-                                            value_bool.unwrap()
-                                    }
-                                    "manga" => {
-                                        preferences.features_enabled.media.manga =
-                                            value_bool.unwrap()
-                                    }
-                                    "anime" => {
-                                        preferences.features_enabled.media.anime =
-                                            value_bool.unwrap()
-                                    }
-                                    "people" => {
-                                        preferences.features_enabled.media.people =
-                                            value_bool.unwrap()
-                                    }
-                                    "groups" => {
-                                        preferences.features_enabled.media.groups =
-                                            value_bool.unwrap()
-                                    }
-                                    "genres" => {
-                                        preferences.features_enabled.media.genres =
-                                            value_bool.unwrap()
-                                    }
-                                    _ => return Err(err()),
-                                };
-                            }
-                            _ => return Err(err()),
-                        }
-                    }
-                    "notifications" => match right {
-                        "to_send" => {
-                            preferences.notifications.to_send =
-                                serde_json::from_str(&input.value).unwrap();
-                        }
-                        "enabled" => {
-                            preferences.notifications.enabled = value_bool.unwrap();
-                        }
-                        _ => return Err(err()),
-                    },
-                    "general" => match right {
-                        "review_scale" => {
-                            preferences.general.review_scale =
-                                UserReviewScale::from_str(&input.value).unwrap();
-                        }
-                        "grid_packing" => {
-                            preferences.general.grid_packing =
-                                GridPacking::from_str(&input.value).unwrap();
-                        }
-                        "display_nsfw" => {
-                            preferences.general.display_nsfw = value_bool.unwrap();
-                        }
-                        "dashboard" => {
-                            preferences.general.dashboard =
-                                serde_json::from_str(&input.value).unwrap();
-                        }
-                        "disable_integrations" => {
-                            preferences.general.disable_integrations = value_bool.unwrap();
-                        }
-                        "persist_queries" => {
-                            preferences.general.persist_queries = value_bool.unwrap();
-                        }
-                        "disable_navigation_animation" => {
-                            preferences.general.disable_navigation_animation = value_bool.unwrap();
-                        }
-                        "disable_videos" => {
-                            preferences.general.disable_videos = value_bool.unwrap();
-                        }
-                        "disable_watch_providers" => {
-                            preferences.general.disable_watch_providers = value_bool.unwrap();
-                        }
-                        "watch_providers" => {
-                            preferences.general.watch_providers =
-                                serde_json::from_str(&input.value).unwrap();
-                        }
-                        "disable_reviews" => {
-                            preferences.general.disable_reviews = value_bool.unwrap();
-                        }
-                        _ => return Err(err()),
-                    },
-                    _ => return Err(err()),
-                };
-            }
-        };
         let mut user_model: user::ActiveModel = user_model.into();
-        user_model.preferences = ActiveValue::Set(preferences);
+        user_model.preferences = ActiveValue::Set(input);
         user_model.update(&self.0.db).await?;
         Ok(true)
     }
diff --git a/crates/utils/common/Cargo.toml b/crates/utils/common/Cargo.toml
index c967903293..854cf663da 100644
--- a/crates/utils/common/Cargo.toml
+++ b/crates/utils/common/Cargo.toml
@@ -11,3 +11,4 @@ enum-models = { path = "../../models/enum" }
 env-utils = { path = "../env" }
 reqwest = { workspace = true }
 serde = { workspace = true }
+tokio = { workspace = true }
diff --git a/crates/utils/common/src/lib.rs b/crates/utils/common/src/lib.rs
index c1e1f538e3..956e78d1ab 100644
--- a/crates/utils/common/src/lib.rs
+++ b/crates/utils/common/src/lib.rs
@@ -5,6 +5,7 @@ use enum_models::{ExerciseLot, MediaLot, MediaSource, WorkoutSetPersonalBest};
 use env_utils::APP_VERSION;
 use reqwest::header::HeaderValue;
 use serde::de;
+use tokio::time::{sleep, Duration};
 
 pub const PROJECT_NAME: &str = "ryot";
 pub const AUTHOR: &str = "ignisda";
@@ -73,25 +74,6 @@ pub const MEDIA_LOT_MAPPINGS: &[(MediaLot, &[MediaSource])] = &[
     (MediaLot::VisualNovel, &[MediaSource::Vndb]),
 ];
 
-#[macro_export]
-macro_rules! ryot_log {
-    (info, $($arg:tt)*) => {
-        tracing::info!(target: "ryot", $($arg)*);
-    };
-    (warn, $($arg:tt)*) => {
-        tracing::warn!(target: "ryot", $($arg)*);
-    };
-    (error, $($arg:tt)*) => {
-        tracing::error!(target: "ryot", $($arg)*);
-    };
-    (debug, $($arg:tt)*) => {
-        tracing::debug!(target: "ryot", $($arg)*);
-    };
-    (trace, $($arg:tt)*) => {
-        tracing::trace!(target: "ryot", $($arg)*);
-    };
-}
-
 pub fn get_first_and_last_day_of_month(year: i32, month: u32) -> (NaiveDate, NaiveDate) {
     let first_day = NaiveDate::from_ymd_opt(year, month, 1).unwrap();
     let last_day = NaiveDate::from_ymd_opt(year, month + 1, 1)
@@ -150,3 +132,39 @@ where
 
     deserializer.deserialize_any(JsonStringVisitor)
 }
+
+pub async fn sleep_for_n_seconds(sec: u64) {
+    sleep(Duration::from_secs(sec)).await;
+}
+
+#[macro_export]
+macro_rules! ryot_log {
+    (info, $($arg:tt)*) => {
+        tracing::info!(target: "ryot", $($arg)*);
+    };
+    (warn, $($arg:tt)*) => {
+        tracing::warn!(target: "ryot", $($arg)*);
+    };
+    (error, $($arg:tt)*) => {
+        tracing::error!(target: "ryot", $($arg)*);
+    };
+    (debug, $($arg:tt)*) => {
+        tracing::debug!(target: "ryot", $($arg)*);
+    };
+    (trace, $($arg:tt)*) => {
+        tracing::trace!(target: "ryot", $($arg)*);
+    };
+}
+
+#[macro_export]
+macro_rules! acquire_lock {
+    ($db:expr, $key:expr) => {
+        use sqlx::postgres::PgAdvisoryLock;
+
+        let key_string = serde_json::to_string($key).unwrap();
+        let lock = PgAdvisoryLock::new(key_string);
+        ryot_log!(debug, "Acquiring advisory lock: {:?}", lock);
+        let conn = $db.get_postgres_connection_pool().acquire().await?;
+        let acquired = lock.acquire(conn).await?;
+    };
+}
diff --git a/crates/utils/dependent/Cargo.toml b/crates/utils/dependent/Cargo.toml
index 47244a077b..b53d532318 100644
--- a/crates/utils/dependent/Cargo.toml
+++ b/crates/utils/dependent/Cargo.toml
@@ -26,6 +26,8 @@ rust_decimal = { workspace = true }
 rust_decimal_macros = { workspace = true }
 sea-orm = { workspace = true }
 serde = { workspace = true }
+serde_json = { workspace = true }
+sqlx = { workspace = true }
 supporting-service = { path = "../../services/supporting" }
 tracing = { workspace = true }
 traits = { path = "../../traits" }
@@ -33,4 +35,4 @@ user-models = { path = "../../models/user" }
 uuid = { workspace = true }
 
 [package.metadata.cargo-machete]
-ignored = ["tracing"]
+ignored = ["serde_json", "sqlx", "tracing"]
diff --git a/crates/utils/dependent/src/lib.rs b/crates/utils/dependent/src/lib.rs
index 0fbe19292a..b5243b3402 100644
--- a/crates/utils/dependent/src/lib.rs
+++ b/crates/utils/dependent/src/lib.rs
@@ -9,7 +9,7 @@ use common_models::{
     MediaStateChanged, MetadataRecentlyConsumedCacheInput, ProgressUpdateCacheInput, StoredUrl,
     StringIdObject, UserLevelCacheKey,
 };
-use common_utils::{ryot_log, EXERCISE_LOT_MAPPINGS, SHOW_SPECIAL_SEASON_NAMES};
+use common_utils::{acquire_lock, ryot_log, EXERCISE_LOT_MAPPINGS, SHOW_SPECIAL_SEASON_NAMES};
 use database_models::{
     collection, collection_to_entity, exercise,
     functions::associate_user_with_entity,
@@ -1239,7 +1239,7 @@ pub async fn progress_update(
     input: ProgressUpdateInput,
     ss: &Arc<SupportingService>,
 ) -> Result<ProgressUpdateResultUnion> {
-    let cache = ApplicationCacheKey::ProgressUpdateCache(UserLevelCacheKey {
+    let cache_and_lock_key = ApplicationCacheKey::ProgressUpdateCache(UserLevelCacheKey {
         user_id: user_id.to_owned(),
         input: ProgressUpdateCacheInput {
             metadata_id: input.metadata_id.clone(),
@@ -1251,10 +1251,11 @@ pub async fn progress_update(
             podcast_episode_number: input.podcast_episode_number,
         },
     });
+    acquire_lock!(&ss.db, &cache_and_lock_key);
     if respect_cache {
         let in_cache = ss
             .cache_service
-            .get_value::<EmptyCacheValue>(cache.clone())
+            .get_value::<EmptyCacheValue>(cache_and_lock_key.clone())
             .await;
         if in_cache.is_some() {
             ryot_log!(debug, "Seen is already in cache");
@@ -1477,7 +1478,7 @@ pub async fn progress_update(
     if seen.state == SeenState::Completed && respect_cache {
         ss.cache_service
             .set_key(
-                cache,
+                cache_and_lock_key,
                 ApplicationCacheValue::Empty(EmptyCacheValue::default()),
             )
             .await?;
diff --git a/crates/utils/specific/Cargo.toml b/crates/utils/specific/Cargo.toml
index 1deb8b2ffd..bc7e34fdaa 100644
--- a/crates/utils/specific/Cargo.toml
+++ b/crates/utils/specific/Cargo.toml
@@ -5,9 +5,9 @@ edition = "2021"
 
 [dependencies]
 anyhow = { workspace = true }
+common-utils = { path = "../common" }
 database-models = { path = "../../models/database" }
 dependent-utils = { path = "../dependent" }
 enum-models = { path = "../../models/enum" }
 sea-orm = { workspace = true }
 supporting-service = { path = "../../services/supporting" }
-tokio = { workspace = true }
diff --git a/crates/utils/specific/src/lib.rs b/crates/utils/specific/src/lib.rs
index 25dd3afd75..5c4062d008 100644
--- a/crates/utils/specific/src/lib.rs
+++ b/crates/utils/specific/src/lib.rs
@@ -1,11 +1,11 @@
 use anyhow::Result;
+use common_utils::sleep_for_n_seconds;
 use database_models::{metadata, prelude::Metadata};
 use dependent_utils::deploy_update_metadata_job;
 use enum_models::{MediaLot, MediaSource};
 use sea_orm::{ColumnTrait, EntityTrait, QueryFilter};
 use std::sync::Arc;
 use supporting_service::SupportingService;
-use tokio::time::{sleep, Duration};
 
 pub mod audiobookshelf {
     use super::*;
@@ -31,7 +31,7 @@ pub mod audiobookshelf {
         let already = get_metadata(identifier, ss).await?;
         if already.podcast_specifics.is_none() {
             deploy_update_metadata_job(&already.id, ss).await.unwrap();
-            sleep(Duration::from_secs(3)).await;
+            sleep_for_n_seconds(3).await;
         }
         get_metadata(identifier, ss).await
     }
diff --git a/libs/generated/src/graphql/backend/gql.ts b/libs/generated/src/graphql/backend/gql.ts
index 93ce94a14d..b0fee4cd3b 100644
--- a/libs/generated/src/graphql/backend/gql.ts
+++ b/libs/generated/src/graphql/backend/gql.ts
@@ -14,8 +14,8 @@ import { TypedDocumentNode as DocumentNode } from '@graphql-typed-document-node/
  * Learn more about it here: https://the-guild.dev/graphql/codegen/plugins/presets/preset-client#reducing-bundle-size
  */
 const documents = {
-    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateComplexJsonInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}": types.RegisterUserDocument,
-    "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}": types.MetadataDetailsDocument,
+    "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation UpdateCustomMetadata($input: UpdateCustomMetadataInput!) {\n  updateCustomMetadata(input: $input)\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UserPreferencesInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation RefreshUserRecommendationsKey {\n  refreshUserRecommendationsKey\n}": types.RegisterUserDocument,
+    "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    createdByUserId\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}": types.MetadataDetailsDocument,
     "query PersonDetails($personId: String!) {\n  personDetails(personId: $personId) {\n    details {\n      id\n      name\n      source\n      identifier\n      isPartial\n      description\n      birthDate\n      deathDate\n      place\n      website\n      gender\n      displayImages\n      sourceUrl\n    }\n    contents {\n      name\n      count\n      items {\n        character\n        metadataId\n      }\n    }\n  }\n}": types.PersonDetailsDocument,
     "query UserAnalytics($input: UserAnalyticsInput!) {\n  userAnalytics(input: $input) {\n    hours {\n      hour\n      entities {\n        entityId\n        entityLot\n        metadataLot\n      }\n    }\n    activities {\n      groupedBy\n      totalCount\n      totalDuration\n      items {\n        day\n        bookCount\n        showCount\n        totalCount\n        movieCount\n        animeCount\n        mangaCount\n        workoutCount\n        podcastCount\n        totalDuration\n        totalBookPages\n        audioBookCount\n        videoGameCount\n        totalReviewCount\n        visualNovelCount\n        totalWorkoutReps\n        totalShowDuration\n        totalMovieDuration\n        totalMetadataCount\n        totalWorkoutWeight\n        totalPodcastDuration\n        totalWorkoutDistance\n        totalWorkoutRestTime\n        totalWorkoutDuration\n        userMeasurementCount\n        totalVideoGameDuration\n        totalAudioBookDuration\n        totalPersonReviewCount\n        totalVisualNovelDuration\n        totalMetadataReviewCount\n        totalWorkoutPersonalBests\n        totalCollectionReviewCount\n        totalMetadataGroupReviewCount\n      }\n    }\n    fitness {\n      workoutReps\n      workoutCount\n      workoutWeight\n      workoutDistance\n      workoutDuration\n      workoutRestTime\n      measurementCount\n      workoutPersonalBests\n      workoutExercises {\n        count\n        exercise\n      }\n      workoutMuscles {\n        count\n        muscle\n      }\n      workoutEquipments {\n        count\n        equipment\n      }\n    }\n  }\n}": types.UserAnalyticsDocument,
     "query UserDetails {\n  userDetails {\n    __typename\n    ... on User {\n      id\n      lot\n      name\n      isDisabled\n      oidcIssuerId\n      preferences {\n        general {\n          reviewScale\n          gridPacking\n          displayNsfw\n          disableVideos\n          persistQueries\n          disableReviews\n          disableIntegrations\n          disableWatchProviders\n          disableNavigationAnimation\n          dashboard {\n            hidden\n            section\n            numElements\n            deduplicateMedia\n          }\n          watchProviders {\n            lot\n            values\n          }\n        }\n        fitness {\n          logging {\n            muteSounds\n            promptForRestTimer\n            showDetailsWhileEditing\n          }\n          exercises {\n            unitSystem\n            setRestTimers {\n              ...SetRestTimersPart\n            }\n          }\n          measurements {\n            custom {\n              name\n              dataType\n            }\n            inbuilt {\n              weight\n              bodyMassIndex\n              totalBodyWater\n              muscle\n              leanBodyMass\n              bodyFat\n              boneMass\n              visceralFat\n              waistCircumference\n              waistToHeightRatio\n              hipCircumference\n              waistToHipRatio\n              chestCircumference\n              thighCircumference\n              bicepsCircumference\n              neckCircumference\n              bodyFatCaliper\n              chestSkinfold\n              abdominalSkinfold\n              thighSkinfold\n              basalMetabolicRate\n              totalDailyEnergyExpenditure\n              calories\n            }\n          }\n        }\n        notifications {\n          toSend\n          enabled\n        }\n        featuresEnabled {\n          analytics {\n            enabled\n          }\n          others {\n            calendar\n            collections\n          }\n          fitness {\n            enabled\n            workouts\n            templates\n            measurements\n          }\n          media {\n            enabled\n            show\n            book\n            anime\n            manga\n            music\n            movie\n            groups\n            people\n            genres\n            podcast\n            audioBook\n            videoGame\n            visualNovel\n          }\n        }\n      }\n    }\n  }\n}": types.UserDetailsDocument,
@@ -43,11 +43,11 @@ export function graphql(source: string): unknown;
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateComplexJsonInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UpdateComplexJsonInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}"];
+export function graphql(source: "mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation UpdateCustomMetadata($input: UpdateCustomMetadataInput!) {\n  updateCustomMetadata(input: $input)\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UserPreferencesInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation RefreshUserRecommendationsKey {\n  refreshUserRecommendationsKey\n}"): (typeof documents)["mutation RegisterUser($input: RegisterUserInput!) {\n  registerUser(input: $input) {\n    __typename\n    ... on RegisterError {\n      error\n    }\n    ... on StringIdObject {\n      id\n    }\n  }\n}\n\nmutation LoginUser($input: AuthUserInput!) {\n  loginUser(input: $input) {\n    __typename\n    ... on LoginError {\n      error\n    }\n    ... on LoginResponse {\n      apiKey\n    }\n  }\n}\n\nmutation AddEntityToCollection($input: ChangeCollectionToEntityInput!) {\n  addEntityToCollection(input: $input)\n}\n\nmutation CommitMetadata($input: CommitMediaInput!) {\n  commitMetadata(input: $input) {\n    id\n  }\n}\n\nmutation CommitMetadataGroup($input: CommitMediaInput!) {\n  commitMetadataGroup(input: $input) {\n    id\n  }\n}\n\nmutation CommitPerson($input: CommitPersonInput!) {\n  commitPerson(input: $input) {\n    id\n  }\n}\n\nmutation CreateCustomExercise($input: ExerciseInput!) {\n  createCustomExercise(input: $input)\n}\n\nmutation UpdateCustomExercise($input: UpdateCustomExerciseInput!) {\n  updateCustomExercise(input: $input)\n}\n\nmutation UpdateUserIntegration($input: UpdateUserIntegrationInput!) {\n  updateUserIntegration(input: $input)\n}\n\nmutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {\n  createCustomMetadata(input: $input) {\n    id\n  }\n}\n\nmutation UpdateCustomMetadata($input: UpdateCustomMetadataInput!) {\n  updateCustomMetadata(input: $input)\n}\n\nmutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {\n  createOrUpdateCollection(input: $input) {\n    id\n  }\n}\n\nmutation CreateReviewComment($input: CreateReviewCommentInput!) {\n  createReviewComment(input: $input)\n}\n\nmutation CreateUserMeasurement($input: UserMeasurementInput!) {\n  createUserMeasurement(input: $input)\n}\n\nmutation CreateUserNotificationPlatform($input: CreateUserNotificationPlatformInput!) {\n  createUserNotificationPlatform(input: $input)\n}\n\nmutation CreateUserIntegration($input: CreateUserIntegrationInput!) {\n  createUserIntegration(input: $input) {\n    id\n  }\n}\n\nmutation CreateOrUpdateUserWorkout($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkout(input: $input)\n}\n\nmutation CreateOrUpdateUserWorkoutTemplate($input: UserWorkoutInput!) {\n  createOrUpdateUserWorkoutTemplate(input: $input)\n}\n\nmutation DeleteCollection($collectionName: String!) {\n  deleteCollection(collectionName: $collectionName)\n}\n\nmutation DeleteReview($reviewId: String!) {\n  deleteReview(reviewId: $reviewId)\n}\n\nmutation DeleteS3Object($key: String!) {\n  deleteS3Object(key: $key)\n}\n\nmutation DeleteSeenItem($seenId: String!) {\n  deleteSeenItem(seenId: $seenId) {\n    id\n  }\n}\n\nmutation DeleteUser($toDeleteUserId: String!) {\n  deleteUser(toDeleteUserId: $toDeleteUserId)\n}\n\nmutation DeleteUserIntegration($integrationId: String!) {\n  deleteUserIntegration(integrationId: $integrationId)\n}\n\nmutation DeleteUserMeasurement($timestamp: DateTime!) {\n  deleteUserMeasurement(timestamp: $timestamp)\n}\n\nmutation DeleteUserNotificationPlatform($notificationId: String!) {\n  deleteUserNotificationPlatform(notificationId: $notificationId)\n}\n\nmutation DeleteUserWorkout($workoutId: String!) {\n  deleteUserWorkout(workoutId: $workoutId)\n}\n\nmutation DeleteUserWorkoutTemplate($workoutTemplateId: String!) {\n  deleteUserWorkoutTemplate(workoutTemplateId: $workoutTemplateId)\n}\n\nmutation DeployBackgroundJob($jobName: BackgroundJob!) {\n  deployBackgroundJob(jobName: $jobName)\n}\n\nmutation DeployBulkProgressUpdate($input: [ProgressUpdateInput!]!) {\n  deployBulkProgressUpdate(input: $input)\n}\n\nmutation DeployExportJob {\n  deployExportJob\n}\n\nmutation DeployImportJob($input: DeployImportJobInput!) {\n  deployImportJob(input: $input)\n}\n\nmutation DeployUpdateMetadataJob($metadataId: String!) {\n  deployUpdateMetadataJob(metadataId: $metadataId)\n}\n\nmutation DeployUpdatePersonJob($personId: String!) {\n  deployUpdatePersonJob(personId: $personId)\n}\n\nmutation DeployUpdateMetadataGroupJob($metadataGroupId: String!) {\n  deployUpdateMetadataGroupJob(metadataGroupId: $metadataGroupId)\n}\n\nmutation UpdateSeenItem($input: UpdateSeenItemInput!) {\n  updateSeenItem(input: $input)\n}\n\nmutation UpdateUserNotificationPlatform($input: UpdateUserNotificationPlatformInput!) {\n  updateUserNotificationPlatform(input: $input)\n}\n\nmutation UpdateUserWorkoutAttributes($input: UpdateUserWorkoutAttributesInput!) {\n  updateUserWorkoutAttributes(input: $input)\n}\n\nmutation GenerateAuthToken {\n  generateAuthToken\n}\n\nmutation MergeMetadata($mergeFrom: String!, $mergeInto: String!) {\n  mergeMetadata(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation DisassociateMetadata($metadataId: String!) {\n  disassociateMetadata(metadataId: $metadataId)\n}\n\nmutation CreateOrUpdateReview($input: CreateOrUpdateReviewInput!) {\n  createOrUpdateReview(input: $input) {\n    id\n  }\n}\n\nmutation PresignedPutS3Url($input: PresignedPutUrlInput!) {\n  presignedPutS3Url(input: $input) {\n    key\n    uploadUrl\n  }\n}\n\nmutation RemoveEntityFromCollection($input: ChangeCollectionToEntityInput!) {\n  removeEntityFromCollection(input: $input) {\n    id\n  }\n}\n\nmutation TestUserNotificationPlatforms {\n  testUserNotificationPlatforms\n}\n\nmutation UpdateUser($input: UpdateUserInput!) {\n  updateUser(input: $input) {\n    id\n  }\n}\n\nmutation UpdateUserPreference($input: UserPreferencesInput!) {\n  updateUserPreference(input: $input)\n}\n\nmutation CreateAccessLink($input: CreateAccessLinkInput!) {\n  createAccessLink(input: $input) {\n    id\n  }\n}\n\nmutation ProcessAccessLink($input: ProcessAccessLinkInput!) {\n  processAccessLink(input: $input) {\n    __typename\n    ... on ProcessAccessLinkError {\n      error\n    }\n    ... on ProcessAccessLinkResponse {\n      apiKey\n      redirectTo\n      tokenValidForDays\n    }\n  }\n}\n\nmutation RevokeAccessLink($accessLinkId: String!) {\n  revokeAccessLink(accessLinkId: $accessLinkId)\n}\n\nmutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {\n  updateUserExerciseSettings(input: $input)\n}\n\nmutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {\n  mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)\n}\n\nmutation RefreshUserRecommendationsKey {\n  refreshUserRecommendationsKey\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
-export function graphql(source: "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}"): (typeof documents)["query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}"];
+export function graphql(source: "query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    createdByUserId\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}"): (typeof documents)["query MetadataDetails($metadataId: String!) {\n  metadataDetails(metadataId: $metadataId) {\n    id\n    lot\n    title\n    source\n    isNsfw\n    isPartial\n    sourceUrl\n    identifier\n    description\n    suggestions\n    publishYear\n    publishDate\n    providerRating\n    createdByUserId\n    productionStatus\n    originalLanguage\n    genres {\n      id\n      name\n    }\n    group {\n      id\n      name\n      part\n    }\n    assets {\n      images\n      videos {\n        videoId\n        source\n      }\n    }\n    creators {\n      name\n      items {\n        id\n        name\n        image\n        character\n      }\n    }\n    watchProviders {\n      name\n      image\n      languages\n    }\n    animeSpecifics {\n      episodes\n    }\n    audioBookSpecifics {\n      runtime\n    }\n    bookSpecifics {\n      pages\n    }\n    movieSpecifics {\n      runtime\n    }\n    mangaSpecifics {\n      volumes\n      chapters\n    }\n    podcastSpecifics {\n      episodes {\n        id\n        title\n        overview\n        thumbnail\n        number\n        runtime\n        publishDate\n      }\n      totalEpisodes\n    }\n    showSpecifics {\n      totalSeasons\n      totalEpisodes\n      runtime\n      seasons {\n        id\n        seasonNumber\n        name\n        overview\n        backdropImages\n        posterImages\n        episodes {\n          id\n          name\n          posterImages\n          episodeNumber\n          publishDate\n          name\n          overview\n          runtime\n        }\n      }\n    }\n    visualNovelSpecifics {\n      length\n    }\n    videoGameSpecifics {\n      platforms\n    }\n    musicSpecifics {\n      duration\n      viewCount\n      byVariousArtists\n    }\n  }\n}"];
 /**
  * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients.
  */
diff --git a/libs/generated/src/graphql/backend/graphql.ts b/libs/generated/src/graphql/backend/graphql.ts
index d22f9ee162..ff961f438b 100644
--- a/libs/generated/src/graphql/backend/graphql.ts
+++ b/libs/generated/src/graphql/backend/graphql.ts
@@ -746,6 +746,7 @@ export type GraphqlMetadataDetails = {
   assets: GraphqlMediaAssets;
   audioBookSpecifics?: Maybe<AudioBookSpecifics>;
   bookSpecifics?: Maybe<BookSpecifics>;
+  createdByUserId?: Maybe<Scalars['String']['output']>;
   creators: Array<MetadataCreatorGroupedByRole>;
   description?: Maybe<Scalars['String']['output']>;
   genres: Array<GenreListItem>;
@@ -1258,6 +1259,8 @@ export type MutationRoot = {
   presignedPutS3Url: PresignedPutUrlResponse;
   /** Get an access token using an access link. */
   processAccessLink: ProcessAccessLinkResult;
+  /** Refresh the user recommendations key. */
+  refreshUserRecommendationsKey: Scalars['Boolean']['output'];
   /**
    * Create a new user for the service. Also set their `lot` as admin if
    * they are the first user.
@@ -1271,6 +1274,8 @@ export type MutationRoot = {
   testUserNotificationPlatforms: Scalars['Boolean']['output'];
   /** Update a custom exercise. */
   updateCustomExercise: Scalars['Boolean']['output'];
+  /** Update custom metadata. */
+  updateCustomMetadata: Scalars['Boolean']['output'];
   /** Update the attributes of a seen item. */
   updateSeenItem: Scalars['Boolean']['output'];
   /** Update a user's profile details. */
@@ -1495,6 +1500,11 @@ export type MutationRootUpdateCustomExerciseArgs = {
 };
 
 
+export type MutationRootUpdateCustomMetadataArgs = {
+  input: UpdateCustomMetadataInput;
+};
+
+
 export type MutationRootUpdateSeenItemArgs = {
   input: UpdateSeenItemInput;
 };
@@ -1521,7 +1531,7 @@ export type MutationRootUpdateUserNotificationPlatformArgs = {
 
 
 export type MutationRootUpdateUserPreferenceArgs = {
-  input: UpdateComplexJsonInput;
+  input: UserPreferencesInput;
 };
 
 
@@ -2095,6 +2105,13 @@ export type SetRestTimersSettings = {
   warmup?: Maybe<Scalars['Int']['output']>;
 };
 
+export type SetRestTimersSettingsInput = {
+  drop?: InputMaybe<Scalars['Int']['input']>;
+  failure?: InputMaybe<Scalars['Int']['input']>;
+  normal?: InputMaybe<Scalars['Int']['input']>;
+  warmup?: InputMaybe<Scalars['Int']['input']>;
+};
+
 /** Details about the statistics of the set performed. */
 export type SetStatisticInput = {
   distance?: InputMaybe<Scalars['Decimal']['input']>;
@@ -2172,12 +2189,6 @@ export type UniqueMediaIdentifier = {
   source: MediaSource;
 };
 
-export type UpdateComplexJsonInput = {
-  /** Dot delimited path to the property that needs to be changed. */
-  property: Scalars['String']['input'];
-  value: Scalars['String']['input'];
-};
-
 export type UpdateCustomExerciseInput = {
   attributes: ExerciseAttributesInput;
   equipment?: InputMaybe<ExerciseEquipment>;
@@ -2191,6 +2202,11 @@ export type UpdateCustomExerciseInput = {
   shouldDelete?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
+export type UpdateCustomMetadataInput = {
+  existingMetadataId: Scalars['String']['input'];
+  update: CreateCustomMetadataInput;
+};
+
 export type UpdateSeenItemInput = {
   finishedOn?: InputMaybe<Scalars['NaiveDate']['input']>;
   manualTimeSpent?: InputMaybe<Scalars['Decimal']['input']>;
@@ -2201,7 +2217,7 @@ export type UpdateSeenItemInput = {
 };
 
 export type UpdateUserExerciseSettings = {
-  change: UpdateComplexJsonInput;
+  change: UserToExerciseSettingsExtraInformationInput;
   exerciseId: Scalars['String']['input'];
 };
 
@@ -2253,6 +2269,10 @@ export type UserAnalyticsFeaturesEnabledPreferences = {
   enabled: Scalars['Boolean']['output'];
 };
 
+export type UserAnalyticsFeaturesEnabledPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+};
+
 export type UserAnalyticsInput = {
   dateRange: ApplicationDateRangeInput;
   groupBy?: InputMaybe<DailyUserActivitiesResponseGroupedBy>;
@@ -2272,6 +2292,11 @@ export enum UserCustomMeasurementDataType {
   Decimal = 'DECIMAL'
 }
 
+export type UserCustomMeasurementInput = {
+  dataType: UserCustomMeasurementDataType;
+  name: Scalars['String']['input'];
+};
+
 export type UserDetailsError = {
   error: UserDetailsErrorVariant;
 };
@@ -2303,6 +2328,13 @@ export type UserFeaturesEnabledPreferences = {
   others: UserOthersFeaturesEnabledPreferences;
 };
 
+export type UserFeaturesEnabledPreferencesInput = {
+  analytics: UserAnalyticsFeaturesEnabledPreferencesInput;
+  fitness: UserFitnessFeaturesEnabledPreferencesInput;
+  media: UserMediaFeaturesEnabledPreferencesInput;
+  others: UserOthersFeaturesEnabledPreferencesInput;
+};
+
 export type UserFitnessAnalytics = {
   measurementCount: Scalars['Int']['output'];
   workoutCount: Scalars['Int']['output'];
@@ -2322,6 +2354,11 @@ export type UserFitnessExercisesPreferences = {
   unitSystem: UserUnitSystem;
 };
 
+export type UserFitnessExercisesPreferencesInput = {
+  setRestTimers: SetRestTimersSettingsInput;
+  unitSystem: UserUnitSystem;
+};
+
 export type UserFitnessFeaturesEnabledPreferences = {
   enabled: Scalars['Boolean']['output'];
   measurements: Scalars['Boolean']['output'];
@@ -2329,23 +2366,47 @@ export type UserFitnessFeaturesEnabledPreferences = {
   workouts: Scalars['Boolean']['output'];
 };
 
+export type UserFitnessFeaturesEnabledPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+  measurements: Scalars['Boolean']['input'];
+  templates: Scalars['Boolean']['input'];
+  workouts: Scalars['Boolean']['input'];
+};
+
 export type UserFitnessLoggingPreferences = {
   muteSounds: Scalars['Boolean']['output'];
   promptForRestTimer: Scalars['Boolean']['output'];
   showDetailsWhileEditing: Scalars['Boolean']['output'];
 };
 
+export type UserFitnessLoggingPreferencesInput = {
+  muteSounds: Scalars['Boolean']['input'];
+  promptForRestTimer: Scalars['Boolean']['input'];
+  showDetailsWhileEditing: Scalars['Boolean']['input'];
+};
+
 export type UserFitnessMeasurementsPreferences = {
   custom: Array<UserCustomMeasurement>;
   inbuilt: UserMeasurementsInBuiltPreferences;
 };
 
+export type UserFitnessMeasurementsPreferencesInput = {
+  custom: Array<UserCustomMeasurementInput>;
+  inbuilt: UserMeasurementsInBuiltPreferencesInput;
+};
+
 export type UserFitnessPreferences = {
   exercises: UserFitnessExercisesPreferences;
   logging: UserFitnessLoggingPreferences;
   measurements: UserFitnessMeasurementsPreferences;
 };
 
+export type UserFitnessPreferencesInput = {
+  exercises: UserFitnessExercisesPreferencesInput;
+  logging: UserFitnessLoggingPreferencesInput;
+  measurements: UserFitnessMeasurementsPreferencesInput;
+};
+
 export type UserGeneralDashboardElement = {
   deduplicateMedia?: Maybe<Scalars['Boolean']['output']>;
   hidden: Scalars['Boolean']['output'];
@@ -2353,6 +2414,13 @@ export type UserGeneralDashboardElement = {
   section: DashboardElementLot;
 };
 
+export type UserGeneralDashboardElementInput = {
+  deduplicateMedia?: InputMaybe<Scalars['Boolean']['input']>;
+  hidden: Scalars['Boolean']['input'];
+  numElements?: InputMaybe<Scalars['Int']['input']>;
+  section: DashboardElementLot;
+};
+
 export type UserGeneralPreferences = {
   dashboard: Array<UserGeneralDashboardElement>;
   disableIntegrations: Scalars['Boolean']['output'];
@@ -2367,11 +2435,30 @@ export type UserGeneralPreferences = {
   watchProviders: Array<UserGeneralWatchProvider>;
 };
 
+export type UserGeneralPreferencesInput = {
+  dashboard: Array<UserGeneralDashboardElementInput>;
+  disableIntegrations: Scalars['Boolean']['input'];
+  disableNavigationAnimation: Scalars['Boolean']['input'];
+  disableReviews: Scalars['Boolean']['input'];
+  disableVideos: Scalars['Boolean']['input'];
+  disableWatchProviders: Scalars['Boolean']['input'];
+  displayNsfw: Scalars['Boolean']['input'];
+  gridPacking: GridPacking;
+  persistQueries: Scalars['Boolean']['input'];
+  reviewScale: UserReviewScale;
+  watchProviders: Array<UserGeneralWatchProviderInput>;
+};
+
 export type UserGeneralWatchProvider = {
   lot: MediaLot;
   values: Array<Scalars['String']['output']>;
 };
 
+export type UserGeneralWatchProviderInput = {
+  lot: MediaLot;
+  values: Array<Scalars['String']['input']>;
+};
+
 export enum UserLot {
   Admin = 'ADMIN',
   Normal = 'NORMAL'
@@ -2483,6 +2570,32 @@ export type UserMeasurementsInBuiltPreferences = {
   weight: Scalars['Boolean']['output'];
 };
 
+export type UserMeasurementsInBuiltPreferencesInput = {
+  abdominalSkinfold: Scalars['Boolean']['input'];
+  basalMetabolicRate: Scalars['Boolean']['input'];
+  bicepsCircumference: Scalars['Boolean']['input'];
+  bodyFat: Scalars['Boolean']['input'];
+  bodyFatCaliper: Scalars['Boolean']['input'];
+  bodyMassIndex: Scalars['Boolean']['input'];
+  boneMass: Scalars['Boolean']['input'];
+  calories: Scalars['Boolean']['input'];
+  chestCircumference: Scalars['Boolean']['input'];
+  chestSkinfold: Scalars['Boolean']['input'];
+  hipCircumference: Scalars['Boolean']['input'];
+  leanBodyMass: Scalars['Boolean']['input'];
+  muscle: Scalars['Boolean']['input'];
+  neckCircumference: Scalars['Boolean']['input'];
+  thighCircumference: Scalars['Boolean']['input'];
+  thighSkinfold: Scalars['Boolean']['input'];
+  totalBodyWater: Scalars['Boolean']['input'];
+  totalDailyEnergyExpenditure: Scalars['Boolean']['input'];
+  visceralFat: Scalars['Boolean']['input'];
+  waistCircumference: Scalars['Boolean']['input'];
+  waistToHeightRatio: Scalars['Boolean']['input'];
+  waistToHipRatio: Scalars['Boolean']['input'];
+  weight: Scalars['Boolean']['input'];
+};
+
 export type UserMeasurementsListInput = {
   endTime?: InputMaybe<Scalars['DateTime']['input']>;
   startTime?: InputMaybe<Scalars['DateTime']['input']>;
@@ -2505,6 +2618,23 @@ export type UserMediaFeaturesEnabledPreferences = {
   visualNovel: Scalars['Boolean']['output'];
 };
 
+export type UserMediaFeaturesEnabledPreferencesInput = {
+  anime: Scalars['Boolean']['input'];
+  audioBook: Scalars['Boolean']['input'];
+  book: Scalars['Boolean']['input'];
+  enabled: Scalars['Boolean']['input'];
+  genres: Scalars['Boolean']['input'];
+  groups: Scalars['Boolean']['input'];
+  manga: Scalars['Boolean']['input'];
+  movie: Scalars['Boolean']['input'];
+  music: Scalars['Boolean']['input'];
+  people: Scalars['Boolean']['input'];
+  podcast: Scalars['Boolean']['input'];
+  show: Scalars['Boolean']['input'];
+  videoGame: Scalars['Boolean']['input'];
+  visualNovel: Scalars['Boolean']['input'];
+};
+
 export type UserMediaNextEntry = {
   chapter?: Maybe<Scalars['Decimal']['output']>;
   episode?: Maybe<Scalars['Int']['output']>;
@@ -2563,11 +2693,21 @@ export type UserNotificationsPreferences = {
   toSend: Array<MediaStateChanged>;
 };
 
+export type UserNotificationsPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+  toSend: Array<MediaStateChanged>;
+};
+
 export type UserOthersFeaturesEnabledPreferences = {
   calendar: Scalars['Boolean']['output'];
   collections: Scalars['Boolean']['output'];
 };
 
+export type UserOthersFeaturesEnabledPreferencesInput = {
+  calendar: Scalars['Boolean']['input'];
+  collections: Scalars['Boolean']['input'];
+};
+
 export type UserPersonDetails = {
   collections: Array<Collection>;
   recentlyConsumed: Scalars['Boolean']['output'];
@@ -2581,6 +2721,13 @@ export type UserPreferences = {
   notifications: UserNotificationsPreferences;
 };
 
+export type UserPreferencesInput = {
+  featuresEnabled: UserFeaturesEnabledPreferencesInput;
+  fitness: UserFitnessPreferencesInput;
+  general: UserGeneralPreferencesInput;
+  notifications: UserNotificationsPreferencesInput;
+};
+
 export enum UserReviewScale {
   OutOfFive = 'OUT_OF_FIVE',
   OutOfHundred = 'OUT_OF_HUNDRED',
@@ -2624,6 +2771,11 @@ export type UserToExerciseSettingsExtraInformation = {
   setRestTimers: SetRestTimersSettings;
 };
 
+export type UserToExerciseSettingsExtraInformationInput = {
+  excludeFromAnalytics: Scalars['Boolean']['input'];
+  setRestTimers: SetRestTimersSettingsInput;
+};
+
 export enum UserToMediaReason {
   Collection = 'COLLECTION',
   Finished = 'FINISHED',
@@ -2933,6 +3085,13 @@ export type CreateCustomMetadataMutationVariables = Exact<{
 
 export type CreateCustomMetadataMutation = { createCustomMetadata: { id: string } };
 
+export type UpdateCustomMetadataMutationVariables = Exact<{
+  input: UpdateCustomMetadataInput;
+}>;
+
+
+export type UpdateCustomMetadataMutation = { updateCustomMetadata: boolean };
+
 export type CreateOrUpdateCollectionMutationVariables = Exact<{
   input: CreateOrUpdateCollectionInput;
 }>;
@@ -3174,7 +3333,7 @@ export type UpdateUserMutationVariables = Exact<{
 export type UpdateUserMutation = { updateUser: { id: string } };
 
 export type UpdateUserPreferenceMutationVariables = Exact<{
-  input: UpdateComplexJsonInput;
+  input: UserPreferencesInput;
 }>;
 
 
@@ -3216,12 +3375,17 @@ export type MergeExerciseMutationVariables = Exact<{
 
 export type MergeExerciseMutation = { mergeExercise: boolean };
 
+export type RefreshUserRecommendationsKeyMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type RefreshUserRecommendationsKeyMutation = { refreshUserRecommendationsKey: boolean };
+
 export type MetadataDetailsQueryVariables = Exact<{
   metadataId: Scalars['String']['input'];
 }>;
 
 
-export type MetadataDetailsQuery = { metadataDetails: { id: string, lot: MediaLot, title: string, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, suggestions: Array<string>, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array<string>, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array<string> }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: string | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array<string>, posterImages: Array<string>, episodes: Array<{ id: number, name: string, posterImages: Array<string>, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array<string> } | null, musicSpecifics?: { duration?: number | null, viewCount?: number | null, byVariousArtists?: boolean | null } | null } };
+export type MetadataDetailsQuery = { metadataDetails: { id: string, lot: MediaLot, title: string, source: MediaSource, isNsfw?: boolean | null, isPartial?: boolean | null, sourceUrl?: string | null, identifier: string, description?: string | null, suggestions: Array<string>, publishYear?: number | null, publishDate?: string | null, providerRating?: string | null, createdByUserId?: string | null, productionStatus?: string | null, originalLanguage?: string | null, genres: Array<{ id: string, name: string }>, group?: { id: string, name: string, part: number } | null, assets: { images: Array<string>, videos: Array<{ videoId: string, source: MetadataVideoSource }> }, creators: Array<{ name: string, items: Array<{ id?: string | null, name: string, image?: string | null, character?: string | null }> }>, watchProviders: Array<{ name: string, image?: string | null, languages: Array<string> }>, animeSpecifics?: { episodes?: number | null } | null, audioBookSpecifics?: { runtime?: number | null } | null, bookSpecifics?: { pages?: number | null } | null, movieSpecifics?: { runtime?: number | null } | null, mangaSpecifics?: { volumes?: number | null, chapters?: string | null } | null, podcastSpecifics?: { totalEpisodes: number, episodes: Array<{ id: string, title: string, overview?: string | null, thumbnail?: string | null, number: number, runtime?: number | null, publishDate: string }> } | null, showSpecifics?: { totalSeasons?: number | null, totalEpisodes?: number | null, runtime?: number | null, seasons: Array<{ id: number, seasonNumber: number, name: string, overview?: string | null, backdropImages: Array<string>, posterImages: Array<string>, episodes: Array<{ id: number, name: string, posterImages: Array<string>, episodeNumber: number, publishDate?: string | null, overview?: string | null, runtime?: number | null }> }> } | null, visualNovelSpecifics?: { length?: number | null } | null, videoGameSpecifics?: { platforms: Array<string> } | null, musicSpecifics?: { duration?: number | null, viewCount?: number | null, byVariousArtists?: boolean | null } | null } };
 
 export type PersonDetailsQueryVariables = Exact<{
   personId: Scalars['String']['input'];
@@ -3548,6 +3712,7 @@ export const CreateCustomExerciseDocument = {"kind":"Document","definitions":[{"
 export const UpdateCustomExerciseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCustomExercise"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCustomExerciseInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomExercise"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateCustomExerciseMutation, UpdateCustomExerciseMutationVariables>;
 export const UpdateUserIntegrationDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserIntegration"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserIntegrationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserIntegration"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserIntegrationMutation, UpdateUserIntegrationMutationVariables>;
 export const CreateCustomMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateCustomMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateCustomMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createCustomMetadata"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<CreateCustomMetadataMutation, CreateCustomMetadataMutationVariables>;
+export const UpdateCustomMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCustomMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCustomMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCustomMetadata"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateCustomMetadataMutation, UpdateCustomMetadataMutationVariables>;
 export const CreateOrUpdateCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOrUpdateCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOrUpdateCollectionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOrUpdateCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<CreateOrUpdateCollectionMutation, CreateOrUpdateCollectionMutationVariables>;
 export const CreateReviewCommentDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateReviewComment"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateReviewCommentInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createReviewComment"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateReviewCommentMutation, CreateReviewCommentMutationVariables>;
 export const CreateUserMeasurementDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateUserMeasurement"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserMeasurementInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createUserMeasurement"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<CreateUserMeasurementMutation, CreateUserMeasurementMutationVariables>;
@@ -3583,13 +3748,14 @@ export const PresignedPutS3UrlDocument = {"kind":"Document","definitions":[{"kin
 export const RemoveEntityFromCollectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RemoveEntityFromCollection"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ChangeCollectionToEntityInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"removeEntityFromCollection"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<RemoveEntityFromCollectionMutation, RemoveEntityFromCollectionMutationVariables>;
 export const TestUserNotificationPlatformsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"TestUserNotificationPlatforms"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"testUserNotificationPlatforms"}}]}}]} as unknown as DocumentNode<TestUserNotificationPlatformsMutation, TestUserNotificationPlatformsMutationVariables>;
 export const UpdateUserDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUser"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUser"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<UpdateUserMutation, UpdateUserMutationVariables>;
-export const UpdateUserPreferenceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserPreference"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateComplexJsonInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserPreference"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserPreferenceMutation, UpdateUserPreferenceMutationVariables>;
+export const UpdateUserPreferenceDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserPreference"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserPreferencesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserPreference"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserPreferenceMutation, UpdateUserPreferenceMutationVariables>;
 export const CreateAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateAccessLinkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<CreateAccessLinkMutation, CreateAccessLinkMutationVariables>;
 export const ProcessAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ProcessAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"processAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkError"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"error"}}]}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ProcessAccessLinkResponse"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"apiKey"}},{"kind":"Field","name":{"kind":"Name","value":"redirectTo"}},{"kind":"Field","name":{"kind":"Name","value":"tokenValidForDays"}}]}}]}}]}}]} as unknown as DocumentNode<ProcessAccessLinkMutation, ProcessAccessLinkMutationVariables>;
 export const RevokeAccessLinkDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RevokeAccessLink"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accessLinkId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"revokeAccessLink"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accessLinkId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accessLinkId"}}}]}]}}]} as unknown as DocumentNode<RevokeAccessLinkMutation, RevokeAccessLinkMutationVariables>;
 export const UpdateUserExerciseSettingsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateUserExerciseSettings"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateUserExerciseSettings"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateUserExerciseSettings"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode<UpdateUserExerciseSettingsMutation, UpdateUserExerciseSettingsMutationVariables>;
 export const MergeExerciseDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"MergeExercise"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"mergeFrom"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"mergeInto"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"mergeExercise"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"mergeFrom"},"value":{"kind":"Variable","name":{"kind":"Name","value":"mergeFrom"}}},{"kind":"Argument","name":{"kind":"Name","value":"mergeInto"},"value":{"kind":"Variable","name":{"kind":"Name","value":"mergeInto"}}}]}]}}]} as unknown as DocumentNode<MergeExerciseMutation, MergeExerciseMutationVariables>;
-export const MetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"isNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"suggestions"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"providerRating"}},{"kind":"Field","name":{"kind":"Name","value":"productionStatus"}},{"kind":"Field","name":{"kind":"Name","value":"originalLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"genres"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"part"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"videoId"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"creators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"character"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"audioBookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"movieSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volumes"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"thumbnail"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalSeasons"}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"seasons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"backdropImages"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"length"}}]}},{"kind":"Field","name":{"kind":"Name","value":"videoGameSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"platforms"}}]}},{"kind":"Field","name":{"kind":"Name","value":"musicSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"viewCount"}},{"kind":"Field","name":{"kind":"Name","value":"byVariousArtists"}}]}}]}}]}}]} as unknown as DocumentNode<MetadataDetailsQuery, MetadataDetailsQueryVariables>;
+export const RefreshUserRecommendationsKeyDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"RefreshUserRecommendationsKey"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"refreshUserRecommendationsKey"}}]}}]} as unknown as DocumentNode<RefreshUserRecommendationsKeyMutation, RefreshUserRecommendationsKeyMutationVariables>;
+export const MetadataDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MetadataDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"metadataDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"metadataId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"metadataId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"isNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"suggestions"}},{"kind":"Field","name":{"kind":"Name","value":"publishYear"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"providerRating"}},{"kind":"Field","name":{"kind":"Name","value":"createdByUserId"}},{"kind":"Field","name":{"kind":"Name","value":"productionStatus"}},{"kind":"Field","name":{"kind":"Name","value":"originalLanguage"}},{"kind":"Field","name":{"kind":"Name","value":"genres"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"group"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"part"}}]}},{"kind":"Field","name":{"kind":"Name","value":"assets"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"images"}},{"kind":"Field","name":{"kind":"Name","value":"videos"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"videoId"}},{"kind":"Field","name":{"kind":"Name","value":"source"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"creators"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"character"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"image"}},{"kind":"Field","name":{"kind":"Name","value":"languages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"animeSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"audioBookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"bookSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pages"}}]}},{"kind":"Field","name":{"kind":"Name","value":"movieSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}},{"kind":"Field","name":{"kind":"Name","value":"mangaSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"volumes"}},{"kind":"Field","name":{"kind":"Name","value":"chapters"}}]}},{"kind":"Field","name":{"kind":"Name","value":"podcastSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"title"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"thumbnail"}},{"kind":"Field","name":{"kind":"Name","value":"number"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}}]}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"totalSeasons"}},{"kind":"Field","name":{"kind":"Name","value":"totalEpisodes"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"seasons"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"seasonNumber"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"backdropImages"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodes"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"posterImages"}},{"kind":"Field","name":{"kind":"Name","value":"episodeNumber"}},{"kind":"Field","name":{"kind":"Name","value":"publishDate"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"overview"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"length"}}]}},{"kind":"Field","name":{"kind":"Name","value":"videoGameSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"platforms"}}]}},{"kind":"Field","name":{"kind":"Name","value":"musicSpecifics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"viewCount"}},{"kind":"Field","name":{"kind":"Name","value":"byVariousArtists"}}]}}]}}]}}]} as unknown as DocumentNode<MetadataDetailsQuery, MetadataDetailsQueryVariables>;
 export const PersonDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PersonDetails"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"personId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"personDetails"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"personId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"personId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"details"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"source"}},{"kind":"Field","name":{"kind":"Name","value":"identifier"}},{"kind":"Field","name":{"kind":"Name","value":"isPartial"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"birthDate"}},{"kind":"Field","name":{"kind":"Name","value":"deathDate"}},{"kind":"Field","name":{"kind":"Name","value":"place"}},{"kind":"Field","name":{"kind":"Name","value":"website"}},{"kind":"Field","name":{"kind":"Name","value":"gender"}},{"kind":"Field","name":{"kind":"Name","value":"displayImages"}},{"kind":"Field","name":{"kind":"Name","value":"sourceUrl"}}]}},{"kind":"Field","name":{"kind":"Name","value":"contents"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"character"}},{"kind":"Field","name":{"kind":"Name","value":"metadataId"}}]}}]}}]}}]}}]} as unknown as DocumentNode<PersonDetailsQuery, PersonDetailsQueryVariables>;
 export const UserAnalyticsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserAnalytics"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UserAnalyticsInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userAnalytics"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hours"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hour"}},{"kind":"Field","name":{"kind":"Name","value":"entities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"entityId"}},{"kind":"Field","name":{"kind":"Name","value":"entityLot"}},{"kind":"Field","name":{"kind":"Name","value":"metadataLot"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"activities"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"groupedBy"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"items"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"day"}},{"kind":"Field","name":{"kind":"Name","value":"bookCount"}},{"kind":"Field","name":{"kind":"Name","value":"showCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalCount"}},{"kind":"Field","name":{"kind":"Name","value":"movieCount"}},{"kind":"Field","name":{"kind":"Name","value":"animeCount"}},{"kind":"Field","name":{"kind":"Name","value":"mangaCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"podcastCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalBookPages"}},{"kind":"Field","name":{"kind":"Name","value":"audioBookCount"}},{"kind":"Field","name":{"kind":"Name","value":"videoGameCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovelCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"totalShowDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalMovieDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalMetadataCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"totalPodcastDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutDuration"}},{"kind":"Field","name":{"kind":"Name","value":"userMeasurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalVideoGameDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalAudioBookDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalPersonReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalVisualNovelDuration"}},{"kind":"Field","name":{"kind":"Name","value":"totalMetadataReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalWorkoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"totalCollectionReviewCount"}},{"kind":"Field","name":{"kind":"Name","value":"totalMetadataGroupReviewCount"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"workoutReps"}},{"kind":"Field","name":{"kind":"Name","value":"workoutCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutWeight"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDistance"}},{"kind":"Field","name":{"kind":"Name","value":"workoutDuration"}},{"kind":"Field","name":{"kind":"Name","value":"workoutRestTime"}},{"kind":"Field","name":{"kind":"Name","value":"measurementCount"}},{"kind":"Field","name":{"kind":"Name","value":"workoutPersonalBests"}},{"kind":"Field","name":{"kind":"Name","value":"workoutExercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"exercise"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutMuscles"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}}]}},{"kind":"Field","name":{"kind":"Name","value":"workoutEquipments"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"count"}},{"kind":"Field","name":{"kind":"Name","value":"equipment"}}]}}]}}]}}]}}]} as unknown as DocumentNode<UserAnalyticsQuery, UserAnalyticsQueryVariables>;
 export const UserDetailsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"UserDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"userDetails"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"__typename"}},{"kind":"InlineFragment","typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"User"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isDisabled"}},{"kind":"Field","name":{"kind":"Name","value":"oidcIssuerId"}},{"kind":"Field","name":{"kind":"Name","value":"preferences"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"general"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"reviewScale"}},{"kind":"Field","name":{"kind":"Name","value":"gridPacking"}},{"kind":"Field","name":{"kind":"Name","value":"displayNsfw"}},{"kind":"Field","name":{"kind":"Name","value":"disableVideos"}},{"kind":"Field","name":{"kind":"Name","value":"persistQueries"}},{"kind":"Field","name":{"kind":"Name","value":"disableReviews"}},{"kind":"Field","name":{"kind":"Name","value":"disableIntegrations"}},{"kind":"Field","name":{"kind":"Name","value":"disableWatchProviders"}},{"kind":"Field","name":{"kind":"Name","value":"disableNavigationAnimation"}},{"kind":"Field","name":{"kind":"Name","value":"dashboard"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hidden"}},{"kind":"Field","name":{"kind":"Name","value":"section"}},{"kind":"Field","name":{"kind":"Name","value":"numElements"}},{"kind":"Field","name":{"kind":"Name","value":"deduplicateMedia"}}]}},{"kind":"Field","name":{"kind":"Name","value":"watchProviders"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lot"}},{"kind":"Field","name":{"kind":"Name","value":"values"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"logging"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"muteSounds"}},{"kind":"Field","name":{"kind":"Name","value":"promptForRestTimer"}},{"kind":"Field","name":{"kind":"Name","value":"showDetailsWhileEditing"}}]}},{"kind":"Field","name":{"kind":"Name","value":"exercises"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"unitSystem"}},{"kind":"Field","name":{"kind":"Name","value":"setRestTimers"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"SetRestTimersPart"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"measurements"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"custom"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"dataType"}}]}},{"kind":"Field","name":{"kind":"Name","value":"inbuilt"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"weight"}},{"kind":"Field","name":{"kind":"Name","value":"bodyMassIndex"}},{"kind":"Field","name":{"kind":"Name","value":"totalBodyWater"}},{"kind":"Field","name":{"kind":"Name","value":"muscle"}},{"kind":"Field","name":{"kind":"Name","value":"leanBodyMass"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFat"}},{"kind":"Field","name":{"kind":"Name","value":"boneMass"}},{"kind":"Field","name":{"kind":"Name","value":"visceralFat"}},{"kind":"Field","name":{"kind":"Name","value":"waistCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHeightRatio"}},{"kind":"Field","name":{"kind":"Name","value":"hipCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"waistToHipRatio"}},{"kind":"Field","name":{"kind":"Name","value":"chestCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"thighCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bicepsCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"neckCircumference"}},{"kind":"Field","name":{"kind":"Name","value":"bodyFatCaliper"}},{"kind":"Field","name":{"kind":"Name","value":"chestSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"abdominalSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"thighSkinfold"}},{"kind":"Field","name":{"kind":"Name","value":"basalMetabolicRate"}},{"kind":"Field","name":{"kind":"Name","value":"totalDailyEnergyExpenditure"}},{"kind":"Field","name":{"kind":"Name","value":"calories"}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"notifications"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"toSend"}},{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"featuresEnabled"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"analytics"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}}]}},{"kind":"Field","name":{"kind":"Name","value":"others"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"calendar"}},{"kind":"Field","name":{"kind":"Name","value":"collections"}}]}},{"kind":"Field","name":{"kind":"Name","value":"fitness"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"workouts"}},{"kind":"Field","name":{"kind":"Name","value":"templates"}},{"kind":"Field","name":{"kind":"Name","value":"measurements"}}]}},{"kind":"Field","name":{"kind":"Name","value":"media"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"enabled"}},{"kind":"Field","name":{"kind":"Name","value":"show"}},{"kind":"Field","name":{"kind":"Name","value":"book"}},{"kind":"Field","name":{"kind":"Name","value":"anime"}},{"kind":"Field","name":{"kind":"Name","value":"manga"}},{"kind":"Field","name":{"kind":"Name","value":"music"}},{"kind":"Field","name":{"kind":"Name","value":"movie"}},{"kind":"Field","name":{"kind":"Name","value":"groups"}},{"kind":"Field","name":{"kind":"Name","value":"people"}},{"kind":"Field","name":{"kind":"Name","value":"genres"}},{"kind":"Field","name":{"kind":"Name","value":"podcast"}},{"kind":"Field","name":{"kind":"Name","value":"audioBook"}},{"kind":"Field","name":{"kind":"Name","value":"videoGame"}},{"kind":"Field","name":{"kind":"Name","value":"visualNovel"}}]}}]}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"SetRestTimersPart"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"SetRestTimersSettings"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"drop"}},{"kind":"Field","name":{"kind":"Name","value":"warmup"}},{"kind":"Field","name":{"kind":"Name","value":"normal"}},{"kind":"Field","name":{"kind":"Name","value":"failure"}}]}}]} as unknown as DocumentNode<UserDetailsQuery, UserDetailsQueryVariables>;
diff --git a/libs/generated/src/graphql/backend/types.generated.ts b/libs/generated/src/graphql/backend/types.generated.ts
index 038c59d715..c9f9ba98fb 100644
--- a/libs/generated/src/graphql/backend/types.generated.ts
+++ b/libs/generated/src/graphql/backend/types.generated.ts
@@ -758,6 +758,7 @@ export type GraphqlMetadataDetails = {
   assets: GraphqlMediaAssets;
   audioBookSpecifics?: Maybe<AudioBookSpecifics>;
   bookSpecifics?: Maybe<BookSpecifics>;
+  createdByUserId?: Maybe<Scalars['String']['output']>;
   creators: Array<MetadataCreatorGroupedByRole>;
   description?: Maybe<Scalars['String']['output']>;
   genres: Array<GenreListItem>;
@@ -1298,6 +1299,8 @@ export type MutationRoot = {
   presignedPutS3Url: PresignedPutUrlResponse;
   /** Get an access token using an access link. */
   processAccessLink: ProcessAccessLinkResult;
+  /** Refresh the user recommendations key. */
+  refreshUserRecommendationsKey: Scalars['Boolean']['output'];
   /**
    * Create a new user for the service. Also set their `lot` as admin if
    * they are the first user.
@@ -1311,6 +1314,8 @@ export type MutationRoot = {
   testUserNotificationPlatforms: Scalars['Boolean']['output'];
   /** Update a custom exercise. */
   updateCustomExercise: Scalars['Boolean']['output'];
+  /** Update custom metadata. */
+  updateCustomMetadata: Scalars['Boolean']['output'];
   /** Update the attributes of a seen item. */
   updateSeenItem: Scalars['Boolean']['output'];
   /** Update a user's profile details. */
@@ -1535,6 +1540,11 @@ export type MutationRootUpdateCustomExerciseArgs = {
 };
 
 
+export type MutationRootUpdateCustomMetadataArgs = {
+  input: UpdateCustomMetadataInput;
+};
+
+
 export type MutationRootUpdateSeenItemArgs = {
   input: UpdateSeenItemInput;
 };
@@ -1561,7 +1571,7 @@ export type MutationRootUpdateUserNotificationPlatformArgs = {
 
 
 export type MutationRootUpdateUserPreferenceArgs = {
-  input: UpdateComplexJsonInput;
+  input: UserPreferencesInput;
 };
 
 
@@ -2162,6 +2172,13 @@ export type SetRestTimersSettings = {
   warmup?: Maybe<Scalars['Int']['output']>;
 };
 
+export type SetRestTimersSettingsInput = {
+  drop?: InputMaybe<Scalars['Int']['input']>;
+  failure?: InputMaybe<Scalars['Int']['input']>;
+  normal?: InputMaybe<Scalars['Int']['input']>;
+  warmup?: InputMaybe<Scalars['Int']['input']>;
+};
+
 /** Details about the statistics of the set performed. */
 export type SetStatisticInput = {
   distance?: InputMaybe<Scalars['Decimal']['input']>;
@@ -2243,12 +2260,6 @@ export type UniqueMediaIdentifier = {
   source: MediaSource;
 };
 
-export type UpdateComplexJsonInput = {
-  /** Dot delimited path to the property that needs to be changed. */
-  property: Scalars['String']['input'];
-  value: Scalars['String']['input'];
-};
-
 export type UpdateCustomExerciseInput = {
   attributes: ExerciseAttributesInput;
   equipment?: InputMaybe<ExerciseEquipment>;
@@ -2262,6 +2273,11 @@ export type UpdateCustomExerciseInput = {
   shouldDelete?: InputMaybe<Scalars['Boolean']['input']>;
 };
 
+export type UpdateCustomMetadataInput = {
+  existingMetadataId: Scalars['String']['input'];
+  update: CreateCustomMetadataInput;
+};
+
 export type UpdateSeenItemInput = {
   finishedOn?: InputMaybe<Scalars['NaiveDate']['input']>;
   manualTimeSpent?: InputMaybe<Scalars['Decimal']['input']>;
@@ -2272,7 +2288,7 @@ export type UpdateSeenItemInput = {
 };
 
 export type UpdateUserExerciseSettings = {
-  change: UpdateComplexJsonInput;
+  change: UserToExerciseSettingsExtraInformationInput;
   exerciseId: Scalars['String']['input'];
 };
 
@@ -2327,6 +2343,10 @@ export type UserAnalyticsFeaturesEnabledPreferences = {
   enabled: Scalars['Boolean']['output'];
 };
 
+export type UserAnalyticsFeaturesEnabledPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+};
+
 export type UserAnalyticsInput = {
   dateRange: ApplicationDateRangeInput;
   groupBy?: InputMaybe<DailyUserActivitiesResponseGroupedBy>;
@@ -2347,6 +2367,11 @@ export enum UserCustomMeasurementDataType {
   Decimal = 'DECIMAL'
 }
 
+export type UserCustomMeasurementInput = {
+  dataType: UserCustomMeasurementDataType;
+  name: Scalars['String']['input'];
+};
+
 export type UserDetailsError = {
   __typename?: 'UserDetailsError';
   error: UserDetailsErrorVariant;
@@ -2381,6 +2406,13 @@ export type UserFeaturesEnabledPreferences = {
   others: UserOthersFeaturesEnabledPreferences;
 };
 
+export type UserFeaturesEnabledPreferencesInput = {
+  analytics: UserAnalyticsFeaturesEnabledPreferencesInput;
+  fitness: UserFitnessFeaturesEnabledPreferencesInput;
+  media: UserMediaFeaturesEnabledPreferencesInput;
+  others: UserOthersFeaturesEnabledPreferencesInput;
+};
+
 export type UserFitnessAnalytics = {
   __typename?: 'UserFitnessAnalytics';
   measurementCount: Scalars['Int']['output'];
@@ -2402,6 +2434,11 @@ export type UserFitnessExercisesPreferences = {
   unitSystem: UserUnitSystem;
 };
 
+export type UserFitnessExercisesPreferencesInput = {
+  setRestTimers: SetRestTimersSettingsInput;
+  unitSystem: UserUnitSystem;
+};
+
 export type UserFitnessFeaturesEnabledPreferences = {
   __typename?: 'UserFitnessFeaturesEnabledPreferences';
   enabled: Scalars['Boolean']['output'];
@@ -2410,6 +2447,13 @@ export type UserFitnessFeaturesEnabledPreferences = {
   workouts: Scalars['Boolean']['output'];
 };
 
+export type UserFitnessFeaturesEnabledPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+  measurements: Scalars['Boolean']['input'];
+  templates: Scalars['Boolean']['input'];
+  workouts: Scalars['Boolean']['input'];
+};
+
 export type UserFitnessLoggingPreferences = {
   __typename?: 'UserFitnessLoggingPreferences';
   muteSounds: Scalars['Boolean']['output'];
@@ -2417,12 +2461,23 @@ export type UserFitnessLoggingPreferences = {
   showDetailsWhileEditing: Scalars['Boolean']['output'];
 };
 
+export type UserFitnessLoggingPreferencesInput = {
+  muteSounds: Scalars['Boolean']['input'];
+  promptForRestTimer: Scalars['Boolean']['input'];
+  showDetailsWhileEditing: Scalars['Boolean']['input'];
+};
+
 export type UserFitnessMeasurementsPreferences = {
   __typename?: 'UserFitnessMeasurementsPreferences';
   custom: Array<UserCustomMeasurement>;
   inbuilt: UserMeasurementsInBuiltPreferences;
 };
 
+export type UserFitnessMeasurementsPreferencesInput = {
+  custom: Array<UserCustomMeasurementInput>;
+  inbuilt: UserMeasurementsInBuiltPreferencesInput;
+};
+
 export type UserFitnessPreferences = {
   __typename?: 'UserFitnessPreferences';
   exercises: UserFitnessExercisesPreferences;
@@ -2430,6 +2485,12 @@ export type UserFitnessPreferences = {
   measurements: UserFitnessMeasurementsPreferences;
 };
 
+export type UserFitnessPreferencesInput = {
+  exercises: UserFitnessExercisesPreferencesInput;
+  logging: UserFitnessLoggingPreferencesInput;
+  measurements: UserFitnessMeasurementsPreferencesInput;
+};
+
 export type UserGeneralDashboardElement = {
   __typename?: 'UserGeneralDashboardElement';
   deduplicateMedia?: Maybe<Scalars['Boolean']['output']>;
@@ -2438,6 +2499,13 @@ export type UserGeneralDashboardElement = {
   section: DashboardElementLot;
 };
 
+export type UserGeneralDashboardElementInput = {
+  deduplicateMedia?: InputMaybe<Scalars['Boolean']['input']>;
+  hidden: Scalars['Boolean']['input'];
+  numElements?: InputMaybe<Scalars['Int']['input']>;
+  section: DashboardElementLot;
+};
+
 export type UserGeneralPreferences = {
   __typename?: 'UserGeneralPreferences';
   dashboard: Array<UserGeneralDashboardElement>;
@@ -2453,12 +2521,31 @@ export type UserGeneralPreferences = {
   watchProviders: Array<UserGeneralWatchProvider>;
 };
 
+export type UserGeneralPreferencesInput = {
+  dashboard: Array<UserGeneralDashboardElementInput>;
+  disableIntegrations: Scalars['Boolean']['input'];
+  disableNavigationAnimation: Scalars['Boolean']['input'];
+  disableReviews: Scalars['Boolean']['input'];
+  disableVideos: Scalars['Boolean']['input'];
+  disableWatchProviders: Scalars['Boolean']['input'];
+  displayNsfw: Scalars['Boolean']['input'];
+  gridPacking: GridPacking;
+  persistQueries: Scalars['Boolean']['input'];
+  reviewScale: UserReviewScale;
+  watchProviders: Array<UserGeneralWatchProviderInput>;
+};
+
 export type UserGeneralWatchProvider = {
   __typename?: 'UserGeneralWatchProvider';
   lot: MediaLot;
   values: Array<Scalars['String']['output']>;
 };
 
+export type UserGeneralWatchProviderInput = {
+  lot: MediaLot;
+  values: Array<Scalars['String']['input']>;
+};
+
 export enum UserLot {
   Admin = 'ADMIN',
   Normal = 'NORMAL'
@@ -2573,6 +2660,32 @@ export type UserMeasurementsInBuiltPreferences = {
   weight: Scalars['Boolean']['output'];
 };
 
+export type UserMeasurementsInBuiltPreferencesInput = {
+  abdominalSkinfold: Scalars['Boolean']['input'];
+  basalMetabolicRate: Scalars['Boolean']['input'];
+  bicepsCircumference: Scalars['Boolean']['input'];
+  bodyFat: Scalars['Boolean']['input'];
+  bodyFatCaliper: Scalars['Boolean']['input'];
+  bodyMassIndex: Scalars['Boolean']['input'];
+  boneMass: Scalars['Boolean']['input'];
+  calories: Scalars['Boolean']['input'];
+  chestCircumference: Scalars['Boolean']['input'];
+  chestSkinfold: Scalars['Boolean']['input'];
+  hipCircumference: Scalars['Boolean']['input'];
+  leanBodyMass: Scalars['Boolean']['input'];
+  muscle: Scalars['Boolean']['input'];
+  neckCircumference: Scalars['Boolean']['input'];
+  thighCircumference: Scalars['Boolean']['input'];
+  thighSkinfold: Scalars['Boolean']['input'];
+  totalBodyWater: Scalars['Boolean']['input'];
+  totalDailyEnergyExpenditure: Scalars['Boolean']['input'];
+  visceralFat: Scalars['Boolean']['input'];
+  waistCircumference: Scalars['Boolean']['input'];
+  waistToHeightRatio: Scalars['Boolean']['input'];
+  waistToHipRatio: Scalars['Boolean']['input'];
+  weight: Scalars['Boolean']['input'];
+};
+
 export type UserMeasurementsListInput = {
   endTime?: InputMaybe<Scalars['DateTime']['input']>;
   startTime?: InputMaybe<Scalars['DateTime']['input']>;
@@ -2596,6 +2709,23 @@ export type UserMediaFeaturesEnabledPreferences = {
   visualNovel: Scalars['Boolean']['output'];
 };
 
+export type UserMediaFeaturesEnabledPreferencesInput = {
+  anime: Scalars['Boolean']['input'];
+  audioBook: Scalars['Boolean']['input'];
+  book: Scalars['Boolean']['input'];
+  enabled: Scalars['Boolean']['input'];
+  genres: Scalars['Boolean']['input'];
+  groups: Scalars['Boolean']['input'];
+  manga: Scalars['Boolean']['input'];
+  movie: Scalars['Boolean']['input'];
+  music: Scalars['Boolean']['input'];
+  people: Scalars['Boolean']['input'];
+  podcast: Scalars['Boolean']['input'];
+  show: Scalars['Boolean']['input'];
+  videoGame: Scalars['Boolean']['input'];
+  visualNovel: Scalars['Boolean']['input'];
+};
+
 export type UserMediaNextEntry = {
   __typename?: 'UserMediaNextEntry';
   chapter?: Maybe<Scalars['Decimal']['output']>;
@@ -2660,12 +2790,22 @@ export type UserNotificationsPreferences = {
   toSend: Array<MediaStateChanged>;
 };
 
+export type UserNotificationsPreferencesInput = {
+  enabled: Scalars['Boolean']['input'];
+  toSend: Array<MediaStateChanged>;
+};
+
 export type UserOthersFeaturesEnabledPreferences = {
   __typename?: 'UserOthersFeaturesEnabledPreferences';
   calendar: Scalars['Boolean']['output'];
   collections: Scalars['Boolean']['output'];
 };
 
+export type UserOthersFeaturesEnabledPreferencesInput = {
+  calendar: Scalars['Boolean']['input'];
+  collections: Scalars['Boolean']['input'];
+};
+
 export type UserPersonDetails = {
   __typename?: 'UserPersonDetails';
   collections: Array<Collection>;
@@ -2681,6 +2821,13 @@ export type UserPreferences = {
   notifications: UserNotificationsPreferences;
 };
 
+export type UserPreferencesInput = {
+  featuresEnabled: UserFeaturesEnabledPreferencesInput;
+  fitness: UserFitnessPreferencesInput;
+  general: UserGeneralPreferencesInput;
+  notifications: UserNotificationsPreferencesInput;
+};
+
 export enum UserReviewScale {
   OutOfFive = 'OUT_OF_FIVE',
   OutOfHundred = 'OUT_OF_HUNDRED',
@@ -2729,6 +2876,11 @@ export type UserToExerciseSettingsExtraInformation = {
   setRestTimers: SetRestTimersSettings;
 };
 
+export type UserToExerciseSettingsExtraInformationInput = {
+  excludeFromAnalytics: Scalars['Boolean']['input'];
+  setRestTimers: SetRestTimersSettingsInput;
+};
+
 export enum UserToMediaReason {
   Collection = 'COLLECTION',
   Finished = 'FINISHED',
diff --git a/libs/graphql/src/backend/mutations/combined.gql b/libs/graphql/src/backend/mutations/combined.gql
index 1d7672d6be..13e5581c71 100644
--- a/libs/graphql/src/backend/mutations/combined.gql
+++ b/libs/graphql/src/backend/mutations/combined.gql
@@ -62,6 +62,10 @@ mutation CreateCustomMetadata($input: CreateCustomMetadataInput!) {
 	}
 }
 
+mutation UpdateCustomMetadata($input: UpdateCustomMetadataInput!) {
+	updateCustomMetadata(input: $input)
+}
+
 mutation CreateOrUpdateCollection($input: CreateOrUpdateCollectionInput!) {
 	createOrUpdateCollection(input: $input) {
 		id
@@ -223,7 +227,7 @@ mutation UpdateUser($input: UpdateUserInput!) {
 	}
 }
 
-mutation UpdateUserPreference($input: UpdateComplexJsonInput!) {
+mutation UpdateUserPreference($input: UserPreferencesInput!) {
 	updateUserPreference(input: $input)
 }
 
@@ -258,3 +262,7 @@ mutation UpdateUserExerciseSettings($input: UpdateUserExerciseSettings!) {
 mutation MergeExercise($mergeFrom: String!, $mergeInto: String!) {
 	mergeExercise(mergeFrom: $mergeFrom, mergeInto: $mergeInto)
 }
+
+mutation RefreshUserRecommendationsKey {
+	refreshUserRecommendationsKey
+}
diff --git a/libs/graphql/src/backend/queries/MetadataDetails.gql b/libs/graphql/src/backend/queries/MetadataDetails.gql
index f9a366fc39..0d012f2919 100644
--- a/libs/graphql/src/backend/queries/MetadataDetails.gql
+++ b/libs/graphql/src/backend/queries/MetadataDetails.gql
@@ -13,6 +13,7 @@ query MetadataDetails($metadataId: String!) {
 		publishYear
 		publishDate
 		providerRating
+		createdByUserId
 		productionStatus
 		originalLanguage
 		genres {
diff --git a/yarn.lock b/yarn.lock
index dc1b6fb921..869c251068 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3857,6 +3857,18 @@ __metadata:
   languageName: node
   linkType: hard
 
+"@mantine/modals@npm:7.14.3":
+  version: 7.14.3
+  resolution: "@mantine/modals@npm:7.14.3"
+  peerDependencies:
+    "@mantine/core": 7.14.3
+    "@mantine/hooks": 7.14.3
+    react: ^18.x || ^19.x
+    react-dom: ^18.x || ^19.x
+  checksum: 10/f513d9b74257074b434f961920868815ba1c7d5b30a7ed7a3e780b2476f484fe54bb52cb5144a2bd97d049cb3b710d81380403061d48124a4e3932ff580a96b2
+  languageName: node
+  linkType: hard
+
 "@mantine/notifications@npm:7.14.3":
   version: 7.14.3
   resolution: "@mantine/notifications@npm:7.14.3"
@@ -5744,6 +5756,7 @@ __metadata:
     "@mantine/dates": "npm:7.14.3"
     "@mantine/form": "npm:7.14.3"
     "@mantine/hooks": "npm:7.14.3"
+    "@mantine/modals": "npm:7.14.3"
     "@mantine/notifications": "npm:7.14.3"
     "@remix-pwa/dev": "npm:3.1.0"
     "@remix-pwa/sw": "npm:3.0.10"
@@ -5788,7 +5801,6 @@ __metadata:
     postcss-preset-mantine: "npm:1.17.0"
     postcss-simple-vars: "npm:7.0.1"
     react: "npm:18.3.1"
-    react-confirm: "npm:0.3.0-7"
     react-dom: "npm:18.3.1"
     react-virtuoso: "npm:4.12.3"
     recharts: "npm:2.14.1"
@@ -14048,16 +14060,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"react-confirm@npm:0.3.0-7":
-  version: 0.3.0-7
-  resolution: "react-confirm@npm:0.3.0-7"
-  peerDependencies:
-    react: 18.x
-    react-dom: 18.x
-  checksum: 10/3da55aaf282758263fd02b619a7b62a3718d17c2ff8b773d054a764d936c568793c0ca5862c48ccc27948119264220f5b2c314e5537653f85a8b6440eec4822d
-  languageName: node
-  linkType: hard
-
 "react-d3-tree@npm:^3.6.2":
   version: 3.6.2
   resolution: "react-d3-tree@npm:3.6.2"


Test patch: