mirror of
https://github.com/ajnart/homarr.git
synced 2026-01-19 22:12:15 +01:00
✨ Readd possibility to search through jellyseerr and overseerr
This commit is contained in:
@@ -16,6 +16,10 @@
|
||||
"access": {
|
||||
"name": "Access",
|
||||
"description": "Configure who has access to your board"
|
||||
},
|
||||
"search": {
|
||||
"name": "Search",
|
||||
"description": "Customize the search experience on your board"
|
||||
}
|
||||
}
|
||||
}
|
||||
6
public/locales/en/settings/customization/search.json
Normal file
6
public/locales/en/settings/customization/search.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"mediaIntegrations": {
|
||||
"label": "Search media integrations",
|
||||
"description": "Selected integrations will be available in the search of this board"
|
||||
}
|
||||
}
|
||||
105
src/components/Board/Customize/Search/SearchCustomization.tsx
Normal file
105
src/components/Board/Customize/Search/SearchCustomization.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
CloseButton,
|
||||
Group,
|
||||
MultiSelect,
|
||||
MultiSelectValueProps,
|
||||
Stack,
|
||||
Text,
|
||||
rem,
|
||||
} from '@mantine/core';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { forwardRef } from 'react';
|
||||
import { IntegrationType, integrationTypes } from '~/server/db/items';
|
||||
import { Integration } from '~/server/db/schema';
|
||||
import { RouterOutputs } from '~/utils/api';
|
||||
|
||||
import { useBoardCustomizationFormContext } from '../form';
|
||||
|
||||
interface IntegrationCustomizationProps {
|
||||
allMediaIntegrations: RouterOutputs['integration']['allMedia'];
|
||||
}
|
||||
|
||||
export const SearchCustomization = ({ allMediaIntegrations }: IntegrationCustomizationProps) => {
|
||||
const { t } = useTranslation('settings/customization/search');
|
||||
const form = useBoardCustomizationFormContext();
|
||||
|
||||
return (
|
||||
<Stack spacing="sm">
|
||||
<MultiSelect
|
||||
{...form.getInputProps('search.mediaIntegrations')}
|
||||
label={t('mediaIntegrations.label')}
|
||||
description={t('mediaIntegrations.description')}
|
||||
searchable
|
||||
valueComponent={IntegrationSelectValue(allMediaIntegrations)}
|
||||
itemComponent={IntegrationSelectItem}
|
||||
data={allMediaIntegrations.map((x) => ({
|
||||
value: x.id,
|
||||
label: x.name,
|
||||
sort: x.sort,
|
||||
}))}
|
||||
/>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
interface ItemProps extends React.ComponentPropsWithoutRef<'div'> {
|
||||
sort: IntegrationType;
|
||||
label: string;
|
||||
}
|
||||
|
||||
const IntegrationSelectItem = forwardRef<HTMLDivElement, ItemProps>(
|
||||
({ label, sort, ...others }: ItemProps, ref) => {
|
||||
return (
|
||||
<div ref={ref} {...others}>
|
||||
<Group noWrap>
|
||||
<Avatar size={20} src={integrationTypes[sort].iconUrl} />
|
||||
|
||||
<Text>{label}</Text>
|
||||
</Group>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
const IntegrationSelectValue =
|
||||
(integrations: Integration[]) =>
|
||||
({
|
||||
value,
|
||||
label,
|
||||
onRemove,
|
||||
classNames,
|
||||
...others
|
||||
}: MultiSelectValueProps & { value: string }) => {
|
||||
const current = integrations.find((x) => x.id === value);
|
||||
return (
|
||||
<div {...others}>
|
||||
<Box
|
||||
sx={(theme) => ({
|
||||
display: 'flex',
|
||||
cursor: 'default',
|
||||
alignItems: 'center',
|
||||
backgroundColor: theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.white,
|
||||
border: `${rem(1)} solid ${
|
||||
theme.colorScheme === 'dark' ? theme.colors.dark[7] : theme.colors.gray[4]
|
||||
}`,
|
||||
paddingLeft: theme.spacing.xs,
|
||||
borderRadius: theme.radius.sm,
|
||||
})}
|
||||
>
|
||||
<Box mr={10}>
|
||||
<Avatar size="xs" src={integrationTypes[current!.sort].iconUrl} />
|
||||
</Box>
|
||||
<Box sx={{ lineHeight: 1, fontSize: rem(12) }}>{label}</Box>
|
||||
<CloseButton
|
||||
onMouseDown={onRemove}
|
||||
variant="transparent"
|
||||
size={22}
|
||||
iconSize={14}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</Box>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -49,10 +49,7 @@ export const Search = ({ isMobile, autoFocus }: SearchProps) => {
|
||||
)
|
||||
.filter(
|
||||
(engine) =>
|
||||
engine.sort !== 'movie' ||
|
||||
board?.sections.some((section) =>
|
||||
section.items.some((x) => x.kind === 'app' && x.integration?.type === engine.value)
|
||||
)
|
||||
engine.sort !== 'movie' || board?.mediaIntegrations.some((x) => x.sort === engine.value)
|
||||
)
|
||||
.map((engine) => ({
|
||||
...engine,
|
||||
@@ -61,6 +58,7 @@ export const Search = ({ isMobile, autoFocus }: SearchProps) => {
|
||||
query: search,
|
||||
}),
|
||||
}));
|
||||
|
||||
const data = [...apps, ...engines];
|
||||
|
||||
return (
|
||||
|
||||
@@ -131,14 +131,6 @@ const MovieDisplay = ({ movie, type }: MovieDisplayProps) => {
|
||||
const { t } = useTranslation('modules/common-media-cards');
|
||||
const [requestModalOpened, requestModal] = useDisclosure(false);
|
||||
|
||||
/*const service = config.apps.find((service) => service.integration.type === type);
|
||||
const mediaUrl = movie.mediaInfo?.plexUrl ?? movie.mediaInfo?.mediaUrl;
|
||||
const serviceUrl = service?.behaviour.externalUrl ?? service?.url;
|
||||
const externalUrl = new URL(
|
||||
`${movie.mediaType}/${movie.id}`,
|
||||
serviceUrl ?? 'https://www.themoviedb.org'
|
||||
);*/
|
||||
|
||||
return (
|
||||
<Card withBorder>
|
||||
<Group noWrap style={{ maxHeight: 250 }} p={0} m={0} spacing="xs" align="stretch">
|
||||
|
||||
@@ -26,41 +26,6 @@ export interface IMedia {
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
export function OverseerrMediaDisplay(props: any) {
|
||||
const { media }: { media: Result } = props;
|
||||
const { config } = useConfigContext();
|
||||
|
||||
if (!config) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const service = config.apps.find(
|
||||
(service) =>
|
||||
service.integration.type === 'overseerr' || service.integration.type === 'jellyseerr'
|
||||
);
|
||||
|
||||
return (
|
||||
<MediaDisplay
|
||||
media={{
|
||||
...media,
|
||||
genres: [],
|
||||
overview: media.overview ?? '',
|
||||
title: media.title ?? media.name ?? media.originalName,
|
||||
poster: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${media.posterPath}`,
|
||||
seasonNumber: media.mediaInfo?.seasons.length,
|
||||
episodetitle: media.title,
|
||||
plexUrl: media.mediaInfo?.plexUrl ?? media.mediaInfo?.mediaUrl,
|
||||
voteAverage: media.voteAverage?.toString(),
|
||||
overseerrResult: media,
|
||||
overseerrId: `${
|
||||
service?.behaviour.externalUrl ? service.behaviour.externalUrl : service?.url
|
||||
}/${media.mediaType}/${media.id}`,
|
||||
type: 'overseer',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function ReadarrMediaDisplay(props: any) {
|
||||
const { media }: { media: any } = props;
|
||||
const { config } = useConfigContext();
|
||||
@@ -192,7 +157,14 @@ export function MediaDisplay({ media }: { media: IMedia }) {
|
||||
|
||||
return (
|
||||
<Group noWrap style={{ maxHeight: 250, maxWidth: 400 }} p={0} m={0} spacing="xs">
|
||||
<Image src={media.poster?? media.altPoster} height={200} width={150} radius="md" fit="cover" withPlaceholder/>
|
||||
<Image
|
||||
src={media.poster ?? media.altPoster}
|
||||
height={200}
|
||||
width={150}
|
||||
radius="md"
|
||||
fit="cover"
|
||||
withPlaceholder
|
||||
/>
|
||||
<Stack justify="space-around">
|
||||
<Stack spacing="sm">
|
||||
<Text lineClamp={2}>
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
IconChartCandle,
|
||||
IconCheck,
|
||||
IconLock,
|
||||
IconSearch,
|
||||
IconX,
|
||||
TablerIconsProps,
|
||||
} from '@tabler/icons-react';
|
||||
@@ -30,18 +31,20 @@ import Link from 'next/link';
|
||||
import { useRouter } from 'next/router';
|
||||
import { ReactNode } from 'react';
|
||||
import { z } from 'zod';
|
||||
import type generalSettingsTranslations from '~/../public/locales/en/settings/customization/general.json';
|
||||
import { AccessCustomization } from '~/components/Board/Customize/Access/AccessCustomization';
|
||||
import { AppearanceCustomization } from '~/components/Board/Customize/Appearance/AppearanceCustomization';
|
||||
import { NetworkCustomization } from '~/components/Board/Customize/Network/NetworkCustomization';
|
||||
import { PageMetadataCustomization } from '~/components/Board/Customize/PageMetadata/PageMetadataCustomization';
|
||||
import { SearchCustomization } from '~/components/Board/Customize/Search/SearchCustomization';
|
||||
import {
|
||||
BoardCustomizationFormProvider,
|
||||
useBoardCustomizationForm,
|
||||
} from '~/components/Board/Customize/form';
|
||||
import { useBoardLink } from '~/components/layout/Templates/BoardLayout';
|
||||
import { MainLayout } from '~/components/layout/Templates/MainLayout';
|
||||
import { createTrpcServersideHelpers } from '~/server/api/helper';
|
||||
import { boardRouter } from '~/server/api/routers/board';
|
||||
import { integrationRouter } from '~/server/api/routers/integration';
|
||||
import { getServerAuthSession } from '~/server/auth';
|
||||
import { getServerSideTranslations } from '~/tools/server/getServerSideTranslations';
|
||||
import { checkForSessionOrAskForLogin } from '~/tools/server/loginBuilder';
|
||||
@@ -54,22 +57,27 @@ const notificationId = 'board-customization-notification';
|
||||
|
||||
export default function CustomizationPage({
|
||||
initialBoard,
|
||||
allMediaIntegrations,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
const query = useRouter().query as { slug: string };
|
||||
const utils = api.useContext();
|
||||
const {
|
||||
data: board,
|
||||
data: queryBoard,
|
||||
isError,
|
||||
error,
|
||||
} = api.boards.byNameSimple.useQuery(
|
||||
{ boardName: query.slug },
|
||||
{
|
||||
initialData: initialBoard,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
useErrorBoundary: false,
|
||||
suspense: false,
|
||||
enabled: typeof window !== 'undefined', // Disable on server-side so it is not cached with an old result.
|
||||
}
|
||||
);
|
||||
|
||||
const board = queryBoard ?? initialBoard; // Initialdata property is not working because it somehow ignores the enabled property.
|
||||
|
||||
const { mutateAsync: updateCustomization, isLoading } =
|
||||
api.boards.updateCustomization.useMutation();
|
||||
const { i18nZodResolver } = useI18nZodResolver();
|
||||
@@ -99,6 +107,9 @@ export default function CustomizationPage({
|
||||
logoSrc: board.logoImageUrl ?? '',
|
||||
faviconSrc: board.faviconImageUrl ?? '',
|
||||
},
|
||||
search: {
|
||||
mediaIntegrations: board.mediaIntegrations.map(({ id }) => id),
|
||||
},
|
||||
},
|
||||
validate: i18nZodResolver(boardCustomizationSchema),
|
||||
validateInputOnChange: true,
|
||||
@@ -250,6 +261,10 @@ export default function CustomizationPage({
|
||||
</Stack>
|
||||
</Grid.Col>
|
||||
</Grid>
|
||||
<Stack spacing="xs">
|
||||
<SectionTitle type="search" icon={IconSearch} />
|
||||
<SearchCustomization allMediaIntegrations={allMediaIntegrations} />
|
||||
</Stack>
|
||||
<Stack spacing="xs">
|
||||
<SectionTitle type="appereance" icon={IconBrush} />
|
||||
<AppearanceCustomization />
|
||||
@@ -264,7 +279,7 @@ export default function CustomizationPage({
|
||||
}
|
||||
|
||||
type SectionTitleProps = {
|
||||
type: 'network' | 'pageMetadata' | 'appereance' | 'access';
|
||||
type: keyof (typeof generalSettingsTranslations)['accordeon'];
|
||||
icon: (props: TablerIconsProps) => ReactNode;
|
||||
};
|
||||
|
||||
@@ -305,20 +320,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
return result;
|
||||
}
|
||||
|
||||
const helpers = await createTrpcServersideHelpers({ req: context.req, res: context.res });
|
||||
const caller = boardRouter.createCaller({
|
||||
const boardCaller = boardRouter.createCaller({
|
||||
session: session,
|
||||
cookies: context.req.cookies,
|
||||
headers: context.req.headers,
|
||||
});
|
||||
|
||||
const board = await caller.byNameSimple({ boardName: routeParams.data.slug });
|
||||
const board = await boardCaller.byNameSimple({ boardName: routeParams.data.slug });
|
||||
if (!board) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const integrationCaller = integrationRouter.createCaller({
|
||||
session: session,
|
||||
cookies: context.req.cookies,
|
||||
headers: context.req.headers,
|
||||
});
|
||||
|
||||
const allMediaIntegrations = await integrationCaller.allMedia();
|
||||
|
||||
const translations = await getServerSideTranslations(
|
||||
[
|
||||
'boards/customize',
|
||||
@@ -329,6 +351,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
'settings/customization/opacity-selector',
|
||||
'settings/customization/gridstack',
|
||||
'settings/customization/access',
|
||||
'settings/customization/search',
|
||||
],
|
||||
context.locale,
|
||||
context.req,
|
||||
@@ -341,6 +364,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
secondaryColor: board.secondaryColor,
|
||||
primaryShade: board.primaryShade,
|
||||
initialBoard: board,
|
||||
allMediaIntegrations,
|
||||
...translations,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -9,6 +9,7 @@ import { dnsHoleRouter } from './routers/dns-hole/router';
|
||||
import { dockerRouter } from './routers/docker/router';
|
||||
import { downloadRouter } from './routers/download';
|
||||
import { iconRouter } from './routers/icon';
|
||||
import { integrationRouter } from './routers/integration';
|
||||
import { inviteRouter } from './routers/invite/invite-router';
|
||||
import { layoutsRouter } from './routers/layout/layout.router';
|
||||
import { mediaRequestsRouter } from './routers/media-request';
|
||||
@@ -49,6 +50,7 @@ export const rootRouter = createTRPCRouter({
|
||||
password: passwordRouter,
|
||||
notebook: notebookRouter,
|
||||
layouts: layoutsRouter,
|
||||
integration: integrationRouter,
|
||||
});
|
||||
|
||||
// export type definition of API
|
||||
|
||||
@@ -9,6 +9,7 @@ import {
|
||||
Integration,
|
||||
appStatusCodes,
|
||||
apps,
|
||||
boardIntegrations,
|
||||
boards,
|
||||
items,
|
||||
layoutItems,
|
||||
@@ -262,26 +263,59 @@ export const boardRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
await db
|
||||
.update(boards)
|
||||
.set({
|
||||
allowGuests: input.customization.access.allowGuests,
|
||||
isPingEnabled: input.customization.network.pingsEnabled,
|
||||
appOpacity: input.customization.appearance.opacity,
|
||||
backgroundImageUrl: input.customization.appearance.backgroundSrc,
|
||||
backgroundImageAttachment: input.customization.appearance.backgroundImageAttachment,
|
||||
backgroundImageSize: input.customization.appearance.backgroundImageSize,
|
||||
backgroundImageRepeat: input.customization.appearance.backgroundImageRepeat,
|
||||
primaryColor: input.customization.appearance.primaryColor,
|
||||
secondaryColor: input.customization.appearance.secondaryColor,
|
||||
customCss: input.customization.appearance.customCss,
|
||||
pageTitle: input.customization.pageMetadata.pageTitle,
|
||||
metaTitle: input.customization.pageMetadata.metaTitle,
|
||||
logoImageUrl: input.customization.pageMetadata.logoSrc,
|
||||
faviconImageUrl: input.customization.pageMetadata.faviconSrc,
|
||||
primaryShade: input.customization.appearance.shade,
|
||||
})
|
||||
.where(eq(boards.id, dbBoard.id));
|
||||
const dbIntegrations = await db.query.boardIntegrations.findMany({
|
||||
where: eq(boardIntegrations.boardId, dbBoard.id),
|
||||
});
|
||||
|
||||
const inputIntegrations = input.customization.search.mediaIntegrations;
|
||||
|
||||
const newIntegrations = inputIntegrations.filter((id) =>
|
||||
dbIntegrations.every((y) => y.integrationId !== id)
|
||||
);
|
||||
|
||||
const removedIntegrations = dbIntegrations.filter((x) =>
|
||||
inputIntegrations.every((y) => y !== x.integrationId)
|
||||
);
|
||||
|
||||
await db.transaction(async (tx) => {
|
||||
await tx
|
||||
.update(boards)
|
||||
.set({
|
||||
allowGuests: input.customization.access.allowGuests,
|
||||
isPingEnabled: input.customization.network.pingsEnabled,
|
||||
appOpacity: input.customization.appearance.opacity,
|
||||
backgroundImageUrl: input.customization.appearance.backgroundSrc,
|
||||
backgroundImageAttachment: input.customization.appearance.backgroundImageAttachment,
|
||||
backgroundImageSize: input.customization.appearance.backgroundImageSize,
|
||||
backgroundImageRepeat: input.customization.appearance.backgroundImageRepeat,
|
||||
primaryColor: input.customization.appearance.primaryColor,
|
||||
secondaryColor: input.customization.appearance.secondaryColor,
|
||||
customCss: input.customization.appearance.customCss,
|
||||
pageTitle: input.customization.pageMetadata.pageTitle,
|
||||
metaTitle: input.customization.pageMetadata.metaTitle,
|
||||
logoImageUrl: input.customization.pageMetadata.logoSrc,
|
||||
faviconImageUrl: input.customization.pageMetadata.faviconSrc,
|
||||
primaryShade: input.customization.appearance.shade,
|
||||
})
|
||||
.where(eq(boards.id, dbBoard.id));
|
||||
|
||||
if (newIntegrations.length > 0) {
|
||||
await tx.insert(boardIntegrations).values(
|
||||
newIntegrations.map((id) => ({
|
||||
boardId: dbBoard.id,
|
||||
integrationId: id,
|
||||
}))
|
||||
);
|
||||
}
|
||||
if (removedIntegrations.length > 0) {
|
||||
await tx.delete(boardIntegrations).where(
|
||||
inArray(
|
||||
boardIntegrations.integrationId,
|
||||
removedIntegrations.map((x) => x.integrationId)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return dbBoard;
|
||||
}),
|
||||
|
||||
13
src/server/api/routers/integration.ts
Normal file
13
src/server/api/routers/integration.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { inArray } from 'drizzle-orm';
|
||||
import { db } from '~/server/db';
|
||||
import { integrations } from '~/server/db/schema';
|
||||
|
||||
import { adminProcedure, createTRPCRouter } from '../trpc';
|
||||
|
||||
export const integrationRouter = createTRPCRouter({
|
||||
allMedia: adminProcedure.query(async () => {
|
||||
return await db.query.integrations.findMany({
|
||||
where: inArray(integrations.sort, ['jellyseerr', 'overseerr']),
|
||||
});
|
||||
}),
|
||||
});
|
||||
@@ -1,11 +1,14 @@
|
||||
import { TRPCError } from '@trpc/server';
|
||||
import axios from 'axios';
|
||||
import Consola from 'consola';
|
||||
import { and, eq } from 'drizzle-orm';
|
||||
import { z } from 'zod';
|
||||
import { MovieResult } from '~/modules/overseerr/Movie';
|
||||
import { Result } from '~/modules/overseerr/SearchResult';
|
||||
import { TvShowResult } from '~/modules/overseerr/TvShow';
|
||||
import { getIntegrations, getSecret } from '~/server/db/queries/integrations';
|
||||
import { db } from '~/server/db';
|
||||
import { getSecret } from '~/server/db/queries/integrations';
|
||||
import { boardIntegrations, integrationSecrets, integrations } from '~/server/db/schema';
|
||||
import { getConfig } from '~/tools/config/getConfig';
|
||||
|
||||
import { createTRPCRouter, publicProcedure } from '../trpc';
|
||||
@@ -20,47 +23,51 @@ export const overseerrRouter = createTRPCRouter({
|
||||
limit: z.number().default(10),
|
||||
})
|
||||
)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const integrations = await getIntegrations(
|
||||
input.boardId,
|
||||
[input.integration],
|
||||
ctx.session?.user
|
||||
);
|
||||
.query(async ({ input }) => {
|
||||
const dbIntegration = await db
|
||||
.select()
|
||||
.from(boardIntegrations)
|
||||
.leftJoin(integrations, eq(integrations.id, boardIntegrations.integrationId))
|
||||
.leftJoin(integrationSecrets, eq(integrationSecrets.integrationId, integrations.id))
|
||||
.where(
|
||||
and(
|
||||
eq(boardIntegrations.boardId, input.boardId),
|
||||
eq(integrations.sort, input.integration)
|
||||
)
|
||||
)
|
||||
.get();
|
||||
|
||||
if (input.query === '' || input.query === undefined || integrations.length === 0) {
|
||||
if (!dbIntegration || !dbIntegration.integration || !dbIntegration.integration_secret) {
|
||||
throw new TRPCError({
|
||||
code: 'NOT_FOUND',
|
||||
message: 'No integration found',
|
||||
});
|
||||
}
|
||||
|
||||
if (input.query === '' || input.query === undefined) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const resultsFromIntegrationApi = await Promise.allSettled<Promise<Result[]>>(
|
||||
integrations.map(async (integration) => {
|
||||
const url = new URL(integration.url);
|
||||
return await axios
|
||||
.get(`${url.origin}/api/v1/search?query=${input.query}`, {
|
||||
headers: {
|
||||
// Set X-Api-Key to the value of the API key
|
||||
'X-Api-Key': getSecret(integration, 'apiKey'),
|
||||
},
|
||||
})
|
||||
.then((res) =>
|
||||
res.data.results.map((x: Result) => ({ ...x, integrationId: integration.id }))
|
||||
)
|
||||
.catch((err) => {
|
||||
Consola.error(err);
|
||||
return [];
|
||||
});
|
||||
const integration = {
|
||||
...dbIntegration.integration,
|
||||
secrets: [dbIntegration.integration_secret!],
|
||||
};
|
||||
const url = new URL(integration.url);
|
||||
const results: Result[] = await axios
|
||||
.get(`${url.origin}/api/v1/search?query=${input.query}`, {
|
||||
headers: {
|
||||
// Set X-Api-Key to the value of the API key
|
||||
'X-Api-Key': getSecret(integration, 'apiKey'),
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const results = resultsFromIntegrationApi
|
||||
.filter(
|
||||
(x): x is PromiseFulfilledResult<(Result & { integrationId: string })[]> =>
|
||||
x.status === 'fulfilled'
|
||||
)
|
||||
.flatMap((x) => x.value);
|
||||
.then((res) => res.data)
|
||||
.catch((err) => {
|
||||
Consola.error(err);
|
||||
return [];
|
||||
});
|
||||
|
||||
return results.slice(0, input.limit).map((result) => ({
|
||||
id: result.id,
|
||||
integrationId: result.integrationId,
|
||||
imageUrl: `https://image.tmdb.org/t/p/w600_and_h900_bestv2/${
|
||||
result.posterPath ?? result.backdropPath
|
||||
}`,
|
||||
|
||||
@@ -179,8 +179,8 @@ export const integrationSecrets = sqliteTable(
|
||||
export const boardIntegrations = sqliteTable(
|
||||
'board_integration',
|
||||
{
|
||||
boardId: text('board_id'),
|
||||
integrationId: text('integration_id'),
|
||||
boardId: text('board_id').notNull(),
|
||||
integrationId: text('integration_id').notNull(),
|
||||
},
|
||||
(boardIntegration) => ({
|
||||
compoundKey: primaryKey(boardIntegration.boardId, boardIntegration.integrationId),
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import { DEFAULT_THEME, MANTINE_COLORS, MantineColor } from '@mantine/core';
|
||||
import { z } from 'zod';
|
||||
import { BackgroundImageAttachment, BackgroundImageRepeat, BackgroundImageSize } from '~/types/settings';
|
||||
import {
|
||||
BackgroundImageAttachment,
|
||||
BackgroundImageRepeat,
|
||||
BackgroundImageSize,
|
||||
} from '~/types/settings';
|
||||
|
||||
export const createBoardSchemaValidation = z.object({
|
||||
name: z.string().min(2).max(25),
|
||||
@@ -37,6 +41,9 @@ export const boardCustomizationSchema = z.object({
|
||||
opacity: z.number().min(10).max(100),
|
||||
customCss: z.string(),
|
||||
}),
|
||||
search: z.object({
|
||||
mediaIntegrations: z.array(z.string()),
|
||||
}),
|
||||
});
|
||||
|
||||
export const boardNameSchema = z.string().regex(/^[a-zA-Z0-9-_]+$/);
|
||||
|
||||
Reference in New Issue
Block a user