diff --git a/.env.example b/.env.example index ae3d1e38b..e78c567a6 100644 --- a/.env.example +++ b/.env.example @@ -37,4 +37,7 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE' TURBO_TELEMETRY_DISABLED=1 # Enable kubernetes tool -# ENABLE_KUBERNETES=true \ No newline at end of file +# ENABLE_KUBERNETES=true + +# Enable mock integration +UNSAFE_ENABLE_MOCK_INTEGRATION=true \ No newline at end of file diff --git a/apps/nextjs/package.json b/apps/nextjs/package.json index 42b6eb5ae..48cab7727 100644 --- a/apps/nextjs/package.json +++ b/apps/nextjs/package.json @@ -29,6 +29,7 @@ "@homarr/db": "workspace:^0.1.0", "@homarr/definitions": "workspace:^0.1.0", "@homarr/docker": "workspace:^0.1.0", + "@homarr/env": "workspace:^0.1.0", "@homarr/form": "workspace:^0.1.0", "@homarr/forms-collection": "workspace:^0.1.0", "@homarr/gridstack": "^1.12.0", diff --git a/apps/nextjs/public/images/mock/avatar.jpg b/apps/nextjs/public/images/mock/avatar.jpg new file mode 100644 index 000000000..c06c8c88c Binary files /dev/null and b/apps/nextjs/public/images/mock/avatar.jpg differ diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx index a697b9270..dd8b0a5de 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/new/_integration-new-dropdown.tsx @@ -10,15 +10,20 @@ import { getIntegrationName, integrationKinds } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; import { IntegrationAvatar } from "@homarr/ui"; -export const IntegrationCreateDropdownContent = () => { +interface IntegrationCreateDropdownContentProps { + enableMockIntegration: boolean; +} + +export const IntegrationCreateDropdownContent = ({ enableMockIntegration }: IntegrationCreateDropdownContentProps) => { const t = useI18n(); const [search, setSearch] = useState(""); const filteredKinds = useMemo(() => { - return integrationKinds.filter((kind) => - getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()), - ); - }, [search]); + return integrationKinds + .filter((kind) => enableMockIntegration || kind !== "mock") + .filter((kind) => getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim())) + .sort((kindA, kindB) => getIntegrationName(kindA).localeCompare(getIntegrationName(kindB))); + }, [search, enableMockIntegration]); const handleSearch = React.useCallback( (event: ChangeEvent) => setSearch(event.target.value), diff --git a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx index d2271a48c..48ead515d 100644 --- a/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx +++ b/apps/nextjs/src/app/[locale]/manage/integrations/page.tsx @@ -41,6 +41,7 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui"; import { ManageContainer } from "~/components/manage/manage-container"; import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb"; import { NoResults } from "~/components/no-results"; +import { env } from "~/env"; import { ActiveTabAccordion } from "../../../../components/active-tab-accordion"; import { DeleteIntegrationActionButton } from "./_integration-buttons"; import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown"; @@ -114,7 +115,7 @@ const IntegrationSelectMenu = ({ children }: PropsWithChildren) => { > {children} - + ); diff --git a/apps/nextjs/src/env.ts b/apps/nextjs/src/env.ts new file mode 100644 index 000000000..8e62d4dc2 --- /dev/null +++ b/apps/nextjs/src/env.ts @@ -0,0 +1,9 @@ +import { createEnv } from "@homarr/env"; +import { createBooleanSchema } from "@homarr/env/schemas"; + +export const env = createEnv({ + server: { + UNSAFE_ENABLE_MOCK_INTEGRATION: createBooleanSchema(false), + }, + experimental__runtimeEnv: process.env, +}); diff --git a/packages/api/src/router/widgets/health-monitoring.ts b/packages/api/src/router/widgets/health-monitoring.ts index c260bef5c..49ebd294b 100644 --- a/packages/api/src/router/widgets/health-monitoring.ts +++ b/packages/api/src/router/widgets/health-monitoring.ts @@ -1,6 +1,6 @@ import { observable } from "@trpc/server/observable"; -import type { HealthMonitoring } from "@homarr/integrations"; +import type { SystemHealthMonitoring } from "@homarr/integrations"; import type { ProxmoxClusterInfo } from "@homarr/integrations/types"; import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring"; @@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc"; export const healthMonitoringRouter = createTRPCRouter({ getSystemHealthStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot")) + .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock")) .query(async ({ ctx }) => { return await Promise.all( ctx.integrations.map(async (integration) => { @@ -26,9 +26,9 @@ export const healthMonitoringRouter = createTRPCRouter({ ); }), subscribeSystemHealthStatus: publicProcedure - .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot")) + .concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock")) .subscription(({ ctx }) => { - return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => { + return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => { const unsubscribes: (() => void)[] = []; for (const integration of ctx.integrations) { const innerHandler = systemInfoRequestHandler.handler(integration, {}); @@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({ }); }), getClusterHealthStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "proxmox")) + .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .query(async ({ ctx }) => { const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {}); const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false }); return data; }), subscribeClusterHealthStatus: publicProcedure - .concat(createOneIntegrationMiddleware("query", "proxmox")) + .concat(createOneIntegrationMiddleware("query", "proxmox", "mock")) .subscription(({ ctx }) => { return observable((emit) => { const unsubscribes: (() => void)[] = []; diff --git a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts index 6e87a57fb..c615aec07 100644 --- a/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts +++ b/packages/cron-jobs/src/jobs/integrations/health-monitoring.ts @@ -8,7 +8,8 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC createRequestIntegrationJobHandler( (integration, itemOptions: Record) => { const { kind } = integration; - if (kind !== "proxmox") { + + if (kind !== "proxmox" && kind !== "mock") { return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions); } return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions); diff --git a/packages/definitions/src/integration.ts b/packages/definitions/src/integration.ts index c127e364f..ca3fdc3da 100644 --- a/packages/definitions/src/integration.ts +++ b/packages/definitions/src/integration.ts @@ -176,6 +176,25 @@ export const integrationDefs = { iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg", category: ["notifications"], }, + // This integration only returns mock data, it is used during development (but can also be used in production by directly going to the create page) + mock: { + name: "Mock", + secretKinds: [[]], + iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/vitest.svg", + category: [ + "calendar", + "dnsHole", + "downloadClient", + "healthMonitoring", + "indexerManager", + "mediaRequest", + "mediaService", + "mediaTranscoding", + "networkController", + "notifications", + "smartHomeServer", + ], + }, } as const satisfies Record; export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf; diff --git a/packages/integrations/src/base/creator.ts b/packages/integrations/src/base/creator.ts index df615fa1f..b1f7cff56 100644 --- a/packages/integrations/src/base/creator.ts +++ b/packages/integrations/src/base/creator.ts @@ -20,6 +20,7 @@ import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration" import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration"; import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration"; import { TdarrIntegration } from "../media-transcoding/tdarr-integration"; +import { MockIntegration } from "../mock/mock-integration"; import { NextcloudIntegration } from "../nextcloud/nextcloud.integration"; import { NTFYIntegration } from "../ntfy/ntfy-integration"; import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration"; @@ -94,6 +95,7 @@ export const integrationCreators = { nextcloud: NextcloudIntegration, unifiController: UnifiControllerIntegration, ntfy: NTFYIntegration, + mock: MockIntegration, } satisfies Record Promise]>; type IntegrationInstanceOfKind = { diff --git a/packages/integrations/src/dashdot/dashdot-integration.ts b/packages/integrations/src/dashdot/dashdot-integration.ts index c569fccef..c5e22fadc 100644 --- a/packages/integrations/src/dashdot/dashdot-integration.ts +++ b/packages/integrations/src/dashdot/dashdot-integration.ts @@ -12,9 +12,10 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { HealthMonitoring } from "../types"; +import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; +import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types"; -export class DashDotIntegration extends Integration { +export class DashDotIntegration extends Integration implements ISystemHealthMonitoringIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/info")); if (!response.ok) return TestConnectionError.StatusResult(response); @@ -26,7 +27,7 @@ export class DashDotIntegration extends Integration { }; } - public async getSystemInfoAsync(): Promise { + public async getSystemInfoAsync(): Promise { const info = await this.getInfoAsync(); const cpuLoad = await this.getCurrentCpuLoadAsync(); const memoryLoad = await this.getCurrentMemoryLoadAsync(); diff --git a/packages/integrations/src/download-client/aria2/aria2-integration.ts b/packages/integrations/src/download-client/aria2/aria2-integration.ts index 07869dcef..8c8482a94 100644 --- a/packages/integrations/src/download-client/aria2/aria2-integration.ts +++ b/packages/integrations/src/download-client/aria2/aria2-integration.ts @@ -4,14 +4,15 @@ import type { fetch as undiciFetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { Aria2Download, Aria2GetClient } from "./aria2-types"; -export class Aria2Integration extends DownloadClientIntegration { +export class Aria2Integration extends Integration implements IDownloadClientIntegration { public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { const client = this.getClient(); const keys: (keyof Aria2Download)[] = [ diff --git a/packages/integrations/src/download-client/deluge/deluge-integration.ts b/packages/integrations/src/download-client/deluge/deluge-integration.ts index 1e054108b..a3310777a 100644 --- a/packages/integrations/src/download-client/deluge/deluge-integration.ts +++ b/packages/integrations/src/download-client/deluge/deluge-integration.ts @@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; @HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) -export class DelugeIntegration extends DownloadClientIntegration { +export class DelugeIntegration extends Integration implements IDownloadClientIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const client = await this.getClientAsync(input.dispatcher); const isSuccess = await client.login(); diff --git a/packages/integrations/src/download-client/nzbget/nzbget-integration.ts b/packages/integrations/src/download-client/nzbget/nzbget-integration.ts index 31f0c6a09..3b799f835 100644 --- a/packages/integrations/src/download-client/nzbget/nzbget-integration.ts +++ b/packages/integrations/src/download-client/nzbget/nzbget-integration.ts @@ -4,15 +4,16 @@ import type { fetch as undiciFetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import type { NzbGetClient } from "./nzbget-types"; -export class NzbGetIntegration extends DownloadClientIntegration { +export class NzbGetIntegration extends Integration implements IDownloadClientIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version"); return { diff --git a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts index 5eeaa2695..1634f39a0 100644 --- a/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts +++ b/packages/integrations/src/download-client/qbittorrent/qbittorrent-integration.ts @@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; @HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) -export class QBitTorrentIntegration extends DownloadClientIntegration { +export class QBitTorrentIntegration extends Integration implements IDownloadClientIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const client = await this.getClientAsync(input.dispatcher); const isSuccess = await client.login(); diff --git a/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts b/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts index 986b5bd4c..42abc58b1 100644 --- a/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts +++ b/packages/integrations/src/download-client/sabnzbd/sabnzbd-integration.ts @@ -5,17 +5,18 @@ import type { fetch as undiciFetch } from "undici"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; import { historySchema, queueSchema } from "./sabnzbd-schema"; dayjs.extend(duration); -export class SabnzbdIntegration extends DownloadClientIntegration { +export class SabnzbdIntegration extends Integration implements IDownloadClientIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { //This is the one call that uses the least amount of data while requiring the api key await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" }); diff --git a/packages/integrations/src/download-client/transmission/transmission-integration.ts b/packages/integrations/src/download-client/transmission/transmission-integration.ts index 29615ed56..fe148dd55 100644 --- a/packages/integrations/src/download-client/transmission/transmission-integration.ts +++ b/packages/integrations/src/download-client/transmission/transmission-integration.ts @@ -6,15 +6,16 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server"; import { HandleIntegrationErrors } from "../../base/errors/decorator"; import { integrationOFetchHttpErrorHandler } from "../../base/errors/http"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; -import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status"; @HandleIntegrationErrors([integrationOFetchHttpErrorHandler]) -export class TransmissionIntegration extends DownloadClientIntegration { +export class TransmissionIntegration extends Integration implements IDownloadClientIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const client = await this.getClientAsync(input.dispatcher); await client.getSession(); diff --git a/packages/integrations/src/emby/emby-integration.ts b/packages/integrations/src/emby/emby-integration.ts index a5642db5f..e05ba96f5 100644 --- a/packages/integrations/src/emby/emby-integration.ts +++ b/packages/integrations/src/emby/emby-integration.ts @@ -7,7 +7,8 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session"; +import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; +import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; import { convertJellyfinType } from "../jellyfin/jellyfin-integration"; const sessionSchema = z.object({ @@ -30,7 +31,7 @@ const sessionSchema = z.object({ UserName: z.string().nullish(), }); -export class EmbyIntegration extends Integration { +export class EmbyIntegration extends Integration implements IMediaServerIntegration { private static readonly apiKeyHeader = "X-Emby-Token"; private static readonly deviceId = "homarr-emby-integration"; private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`; diff --git a/packages/integrations/src/homeassistant/homeassistant-integration.ts b/packages/integrations/src/homeassistant/homeassistant-integration.ts index 5fe767d8a..714e24e77 100644 --- a/packages/integrations/src/homeassistant/homeassistant-integration.ts +++ b/packages/integrations/src/homeassistant/homeassistant-integration.ts @@ -5,9 +5,10 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration"; import { entityStateSchema } from "./homeassistant-types"; -export class HomeAssistantIntegration extends Integration { +export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration { public async getEntityStateAsync(entityId: string) { try { const response = await this.getAsync(`/api/states/${entityId}`); @@ -15,6 +16,7 @@ export class HomeAssistantIntegration extends Integration { if (!response.ok) { logger.warn(`Response did not indicate success`); return { + success: false as const, error: "Response did not indicate success", }; } diff --git a/packages/integrations/src/index.ts b/packages/integrations/src/index.ts index ed6901cf5..b697ce852 100644 --- a/packages/integrations/src/index.ts +++ b/packages/integrations/src/index.ts @@ -7,7 +7,6 @@ export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorren export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration"; export { TransmissionIntegration } from "./download-client/transmission/transmission-integration"; export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration"; -export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration"; export { JellyfinIntegration } from "./jellyfin/jellyfin-integration"; export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration"; export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration"; @@ -28,14 +27,17 @@ export type { IntegrationInput } from "./base/integration"; export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data"; export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items"; export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status"; -export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring"; -export { MediaRequestStatus } from "./interfaces/media-requests/media-request"; -export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request"; -export type { StreamSession } from "./interfaces/media-server/session"; -export type { TdarrQueue } from "./interfaces/media-transcoding/queue"; -export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics"; -export type { TdarrWorker } from "./interfaces/media-transcoding/workers"; -export type { Notification } from "./interfaces/notifications/notification"; +export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types"; +export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types"; +export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types"; +export type { StreamSession } from "./interfaces/media-server/media-server-types"; +export type { + TdarrQueue, + TdarrPieSegment, + TdarrStatistics, + TdarrWorker, +} from "./interfaces/media-transcoding/media-transcoding-types"; +export type { Notification } from "./interfaces/notifications/notification-types"; // Schemas export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items"; diff --git a/packages/integrations/src/interfaces/calendar/calendar-integration.ts b/packages/integrations/src/interfaces/calendar/calendar-integration.ts new file mode 100644 index 000000000..39afae97e --- /dev/null +++ b/packages/integrations/src/interfaces/calendar/calendar-integration.ts @@ -0,0 +1,5 @@ +import type { CalendarEvent } from "./calendar-types"; + +export interface ICalendarIntegration { + getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored: boolean): Promise; +} diff --git a/packages/integrations/src/calendar-types.ts b/packages/integrations/src/interfaces/calendar/calendar-types.ts similarity index 100% rename from packages/integrations/src/calendar-types.ts rename to packages/integrations/src/interfaces/calendar/calendar-types.ts diff --git a/packages/integrations/src/interfaces/downloads/download-client-integration.ts b/packages/integrations/src/interfaces/downloads/download-client-integration.ts index 97b4844ab..bc43bee27 100644 --- a/packages/integrations/src/interfaces/downloads/download-client-integration.ts +++ b/packages/integrations/src/interfaces/downloads/download-client-integration.ts @@ -1,18 +1,17 @@ -import { Integration } from "../../base/integration"; import type { DownloadClientJobsAndStatus } from "./download-client-data"; import type { DownloadClientItem } from "./download-client-items"; -export abstract class DownloadClientIntegration extends Integration { +export interface IDownloadClientIntegration { /** Get download client's status and list of all of it's items */ - public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise; + getClientJobsAndStatusAsync(input: { limit: number }): Promise; /** Pauses the client or all of it's items */ - public abstract pauseQueueAsync(): Promise; + pauseQueueAsync(): Promise; /** Pause a single item using it's ID */ - public abstract pauseItemAsync(item: DownloadClientItem): Promise; + pauseItemAsync(item: DownloadClientItem): Promise; /** Resumes the client or all of it's items */ - public abstract resumeQueueAsync(): Promise; + resumeQueueAsync(): Promise; /** Resume a single item using it's ID */ - public abstract resumeItemAsync(item: DownloadClientItem): Promise; + resumeItemAsync(item: DownloadClientItem): Promise; /** Delete an entry on the client or a file from disk */ - public abstract deleteItemAsync(item: DownloadClientItem, fromDisk: boolean): Promise; + deleteItemAsync(item: DownloadClientItem, fromDisk: boolean): Promise; } diff --git a/packages/integrations/src/interfaces/health-monitoring/health-monitoring-integration.ts b/packages/integrations/src/interfaces/health-monitoring/health-monitoring-integration.ts new file mode 100644 index 000000000..bef0b3573 --- /dev/null +++ b/packages/integrations/src/interfaces/health-monitoring/health-monitoring-integration.ts @@ -0,0 +1,9 @@ +import type { ClusterHealthMonitoring, SystemHealthMonitoring } from "./health-monitoring-types"; + +export interface ISystemHealthMonitoringIntegration { + getSystemInfoAsync(): Promise; +} + +export interface IClusterHealthMonitoringIntegration { + getClusterInfoAsync(): Promise; +} diff --git a/packages/integrations/src/interfaces/health-monitoring/healt-monitoring.ts b/packages/integrations/src/interfaces/health-monitoring/health-monitoring-types.ts similarity index 59% rename from packages/integrations/src/interfaces/health-monitoring/healt-monitoring.ts rename to packages/integrations/src/interfaces/health-monitoring/health-monitoring-types.ts index 90b2b8013..5935ba9e4 100644 --- a/packages/integrations/src/interfaces/health-monitoring/healt-monitoring.ts +++ b/packages/integrations/src/interfaces/health-monitoring/health-monitoring-types.ts @@ -1,4 +1,6 @@ -export interface HealthMonitoring { +import type { LxcResource, NodeResource, QemuResource, StorageResource } from "../../types"; + +export interface SystemHealthMonitoring { version: string; cpuModelName: string; cpuUtilization: number; @@ -25,3 +27,11 @@ export interface HealthMonitoring { overallStatus: string; }[]; } + +// TODO: in the future decouple this from the Proxmox integration +export interface ClusterHealthMonitoring { + nodes: NodeResource[]; + lxcs: LxcResource[]; + vms: QemuResource[]; + storages: StorageResource[]; +} diff --git a/packages/integrations/src/interfaces/indexer-manager/indexer-manager-integration.ts b/packages/integrations/src/interfaces/indexer-manager/indexer-manager-integration.ts new file mode 100644 index 000000000..d4b9356b4 --- /dev/null +++ b/packages/integrations/src/interfaces/indexer-manager/indexer-manager-integration.ts @@ -0,0 +1,6 @@ +import type { Indexer } from "./indexer-manager-types"; + +export interface IIndexerManagerIntegration { + getIndexersAsync(): Promise; + testAllAsync(): Promise; +} diff --git a/packages/integrations/src/interfaces/indexer-manager/indexer.ts b/packages/integrations/src/interfaces/indexer-manager/indexer-manager-types.ts similarity index 100% rename from packages/integrations/src/interfaces/indexer-manager/indexer.ts rename to packages/integrations/src/interfaces/indexer-manager/indexer-manager-types.ts diff --git a/packages/integrations/src/interfaces/media-requests/media-request-integration.ts b/packages/integrations/src/interfaces/media-requests/media-request-integration.ts new file mode 100644 index 000000000..1b598ef16 --- /dev/null +++ b/packages/integrations/src/interfaces/media-requests/media-request-integration.ts @@ -0,0 +1,11 @@ +import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "./media-request-types"; + +export interface IMediaRequestIntegration { + getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise; + requestMediaAsync(mediaType: "movie" | "tv", id: number, seasons?: number[]): Promise; + getRequestsAsync(): Promise; + getStatsAsync(): Promise; + getUsersAsync(): Promise; + approveRequestAsync(requestId: number): Promise; + declineRequestAsync(requestId: number): Promise; +} diff --git a/packages/integrations/src/interfaces/media-requests/media-request.ts b/packages/integrations/src/interfaces/media-requests/media-request-types.ts similarity index 77% rename from packages/integrations/src/interfaces/media-requests/media-request.ts rename to packages/integrations/src/interfaces/media-requests/media-request-types.ts index d4a449727..15910eaf5 100644 --- a/packages/integrations/src/interfaces/media-requests/media-request.ts +++ b/packages/integrations/src/interfaces/media-requests/media-request-types.ts @@ -1,3 +1,25 @@ +interface SerieSeason { + id: number; + seasonNumber: number; + name: string; + episodeCount: number; +} + +interface SeriesInformation { + id: number; + overview: string; + seasons: SerieSeason[]; + posterPath: string; +} + +interface MovieInformation { + id: number; + overview: string; + posterPath: string; +} + +export type MediaInformation = SeriesInformation | MovieInformation; + export interface MediaRequest { id: number; name: string; diff --git a/packages/integrations/src/interfaces/media-server/media-server-integration.ts b/packages/integrations/src/interfaces/media-server/media-server-integration.ts new file mode 100644 index 000000000..07f1a9946 --- /dev/null +++ b/packages/integrations/src/interfaces/media-server/media-server-integration.ts @@ -0,0 +1,5 @@ +import type { CurrentSessionsInput, StreamSession } from "./media-server-types"; + +export interface IMediaServerIntegration { + getCurrentSessionsAsync(options: CurrentSessionsInput): Promise; +} diff --git a/packages/integrations/src/interfaces/media-server/session.ts b/packages/integrations/src/interfaces/media-server/media-server-types.ts similarity index 100% rename from packages/integrations/src/interfaces/media-server/session.ts rename to packages/integrations/src/interfaces/media-server/media-server-types.ts diff --git a/packages/integrations/src/interfaces/media-transcoding/media-transcoding-integration.ts b/packages/integrations/src/interfaces/media-transcoding/media-transcoding-integration.ts new file mode 100644 index 000000000..d318f3400 --- /dev/null +++ b/packages/integrations/src/interfaces/media-transcoding/media-transcoding-integration.ts @@ -0,0 +1,7 @@ +import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "./media-transcoding-types"; + +export interface IMediaTranscodingIntegration { + getStatisticsAsync(): Promise; + getWorkersAsync(): Promise; + getQueueAsync(firstItemIndex: number, pageSize: number): Promise; +} diff --git a/packages/integrations/src/interfaces/media-transcoding/statistics.ts b/packages/integrations/src/interfaces/media-transcoding/media-transcoding-types.ts similarity index 52% rename from packages/integrations/src/interfaces/media-transcoding/statistics.ts rename to packages/integrations/src/interfaces/media-transcoding/media-transcoding-types.ts index 916516b77..c73c4393c 100644 --- a/packages/integrations/src/interfaces/media-transcoding/statistics.ts +++ b/packages/integrations/src/interfaces/media-transcoding/media-transcoding-types.ts @@ -1,3 +1,20 @@ +export interface TdarrQueue { + array: { + id: string; + healthCheck: string; + transcode: string; + filePath: string; + fileSize: number; + container: string; + codec: string; + resolution: string; + type: "transcode" | "health-check"; + }[]; + totalCount: number; + startIndex: number; + endIndex: number; +} + export interface TdarrPieSegment { name: string; value: number; @@ -21,3 +38,17 @@ export interface TdarrStatistics { audioCodecs: TdarrPieSegment[]; audioContainers: TdarrPieSegment[]; } + +export interface TdarrWorker { + id: string; + filePath: string; + fps: number; + percentage: number; + ETA: string; + jobType: string; + status: string; + step: string; + originalSize: number; + estimatedSize: number | null; + outputSize: number | null; +} diff --git a/packages/integrations/src/interfaces/media-transcoding/queue.ts b/packages/integrations/src/interfaces/media-transcoding/queue.ts deleted file mode 100644 index 1b85dddc9..000000000 --- a/packages/integrations/src/interfaces/media-transcoding/queue.ts +++ /dev/null @@ -1,16 +0,0 @@ -export interface TdarrQueue { - array: { - id: string; - healthCheck: string; - transcode: string; - filePath: string; - fileSize: number; - container: string; - codec: string; - resolution: string; - type: "transcode" | "health-check"; - }[]; - totalCount: number; - startIndex: number; - endIndex: number; -} diff --git a/packages/integrations/src/interfaces/media-transcoding/workers.ts b/packages/integrations/src/interfaces/media-transcoding/workers.ts deleted file mode 100644 index 9adaed716..000000000 --- a/packages/integrations/src/interfaces/media-transcoding/workers.ts +++ /dev/null @@ -1,13 +0,0 @@ -export interface TdarrWorker { - id: string; - filePath: string; - fps: number; - percentage: number; - ETA: string; - jobType: string; - status: string; - step: string; - originalSize: number; - estimatedSize: number | null; - outputSize: number | null; -} diff --git a/packages/integrations/src/interfaces/notifications/notification.ts b/packages/integrations/src/interfaces/notifications/notification-types.ts similarity index 100% rename from packages/integrations/src/interfaces/notifications/notification.ts rename to packages/integrations/src/interfaces/notifications/notification-types.ts diff --git a/packages/integrations/src/interfaces/notifications/notifications-integration.ts b/packages/integrations/src/interfaces/notifications/notifications-integration.ts index 45a052d27..a02e70b47 100644 --- a/packages/integrations/src/interfaces/notifications/notifications-integration.ts +++ b/packages/integrations/src/interfaces/notifications/notifications-integration.ts @@ -1,6 +1,5 @@ -import { Integration } from "../../base/integration"; -import type { Notification } from "./notification"; +import type { Notification } from "./notification-types"; -export abstract class NotificationsIntegration extends Integration { - public abstract getNotificationsAsync(): Promise; +export interface INotificationsIntegration { + getNotificationsAsync(): Promise; } diff --git a/packages/integrations/src/interfaces/smart-home/smart-home-integration.ts b/packages/integrations/src/interfaces/smart-home/smart-home-integration.ts new file mode 100644 index 000000000..0cd9e8eb8 --- /dev/null +++ b/packages/integrations/src/interfaces/smart-home/smart-home-integration.ts @@ -0,0 +1,7 @@ +import type { EntityStateResult } from "./smart-home-types"; + +export interface ISmartHomeIntegration { + getEntityStateAsync(entityId: string): Promise; + triggerAutomationAsync(entityId: string): Promise; + triggerToggleAsync(entityId: string): Promise; +} diff --git a/packages/integrations/src/interfaces/smart-home/smart-home-types.ts b/packages/integrations/src/interfaces/smart-home/smart-home-types.ts new file mode 100644 index 000000000..efc113dc7 --- /dev/null +++ b/packages/integrations/src/interfaces/smart-home/smart-home-types.ts @@ -0,0 +1,17 @@ +interface EntityState { + attributes: Record; + entity_id: string; + last_changed: Date; + last_updated: Date; + state: string; +} + +export type EntityStateResult = + | { + success: true; + data: EntityState; + } + | { + success: false; + error: unknown; + }; diff --git a/packages/integrations/src/jellyfin/jellyfin-integration.ts b/packages/integrations/src/jellyfin/jellyfin-integration.ts index 35c18df35..5bfcb2e3f 100644 --- a/packages/integrations/src/jellyfin/jellyfin-integration.ts +++ b/packages/integrations/src/jellyfin/jellyfin-integration.ts @@ -11,10 +11,11 @@ import { integrationAxiosHttpErrorHandler } from "../base/errors/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session"; +import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; +import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; @HandleIntegrationErrors([integrationAxiosHttpErrorHandler]) -export class JellyfinIntegration extends Integration { +export class JellyfinIntegration extends Integration implements IMediaServerIntegration { private readonly jellyfin: Jellyfin = new Jellyfin({ clientInfo: { name: "Homarr", diff --git a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts index f4107c61d..bec3fde58 100644 --- a/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts +++ b/packages/integrations/src/media-organizer/lidarr/lidarr-integration.ts @@ -3,13 +3,15 @@ import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { logger } from "@homarr/log"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; -import type { CalendarEvent } from "../../calendar-types"; -import { MediaOrganizerIntegration } from "../media-organizer-integration"; +import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; +import { mediaOrganizerPriorities } from "../media-organizer"; -export class LidarrIntegration extends MediaOrganizerIntegration { +export class LidarrIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { headers: { "X-Api-Key": super.getSecretValue("apiKey") }, @@ -103,7 +105,8 @@ export class LidarrIntegration extends MediaOrganizerIntegration { const flatImages = [...event.images]; const sortedImages = flatImages.sort( - (imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType), + (imageA, imageB) => + mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType), ); logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`); return sortedImages[0]; diff --git a/packages/integrations/src/media-organizer/media-organizer-integration.ts b/packages/integrations/src/media-organizer/media-organizer-integration.ts deleted file mode 100644 index a576cd92f..000000000 --- a/packages/integrations/src/media-organizer/media-organizer-integration.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { Integration } from "../base/integration"; - -export abstract class MediaOrganizerIntegration extends Integration { - /** - * Priority list that determines the quality of images using their order. - * Types at the start of the list are better than those at the end. - * We do this to attempt to find the best quality image for the show. - */ - protected readonly priorities: string[] = [ - "cover", // Official, perfect aspect ratio, best for music - "poster", // Official, perfect aspect ratio - "banner", // Official, bad aspect ratio - "disc", // Official, second best for music / books - "logo", // Official, possibly unrelated - "fanart", // Unofficial, possibly bad quality - "screenshot", // Bad aspect ratio, possibly bad quality - "clearlogo", // Without background, bad aspect ratio, - "headshot", // Unrelated - "unknown", // Not known, possibly good or bad, better not to choose - ]; -} diff --git a/packages/integrations/src/media-organizer/media-organizer.ts b/packages/integrations/src/media-organizer/media-organizer.ts new file mode 100644 index 000000000..dbcb29535 --- /dev/null +++ b/packages/integrations/src/media-organizer/media-organizer.ts @@ -0,0 +1,17 @@ +/** + * Priority list that determines the quality of images using their order. + * Types at the start of the list are better than those at the end. + * We do this to attempt to find the best quality image for the show. + */ +export const mediaOrganizerPriorities = [ + "cover", // Official, perfect aspect ratio, best for music + "poster", // Official, perfect aspect ratio + "banner", // Official, bad aspect ratio + "disc", // Official, second best for music / books + "logo", // Official, possibly unrelated + "fanart", // Unofficial, possibly bad quality + "screenshot", // Bad aspect ratio, possibly bad quality + "clearlogo", // Without background, bad aspect ratio, + "headshot", // Unrelated + "unknown", // Not known, possibly good or bad, better not to choose +]; diff --git a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts index e4777f69a..681ade5a0 100644 --- a/packages/integrations/src/media-organizer/radarr/radarr-integration.ts +++ b/packages/integrations/src/media-organizer/radarr/radarr-integration.ts @@ -4,14 +4,16 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import type { AtLeastOneOf } from "@homarr/common/types"; import { logger } from "@homarr/log"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; -import type { CalendarEvent } from "../../calendar-types"; -import { radarrReleaseTypes } from "../../calendar-types"; -import { MediaOrganizerIntegration } from "../media-organizer-integration"; +import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; +import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types"; +import { mediaOrganizerPriorities } from "../media-organizer"; -export class RadarrIntegration extends MediaOrganizerIntegration { +export class RadarrIntegration extends Integration implements ICalendarIntegration { /** * Gets the events in the Radarr calendar between two dates. * @param start The start date @@ -82,7 +84,8 @@ export class RadarrIntegration extends MediaOrganizerIntegration { const flatImages = [...event.images]; const sortedImages = flatImages.sort( - (imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType), + (imageA, imageB) => + mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType), ); logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`); return sortedImages[0]; diff --git a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts index 507bde1ae..bd288db61 100644 --- a/packages/integrations/src/media-organizer/readarr/readarr-integration.ts +++ b/packages/integrations/src/media-organizer/readarr/readarr-integration.ts @@ -3,13 +3,15 @@ import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { logger } from "@homarr/log"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; -import type { CalendarEvent } from "../../calendar-types"; -import { MediaOrganizerIntegration } from "../media-organizer-integration"; +import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; +import { mediaOrganizerPriorities } from "../media-organizer"; -export class ReadarrIntegration extends MediaOrganizerIntegration { +export class ReadarrIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api"), { headers: { "X-Api-Key": super.getSecretValue("apiKey") }, @@ -81,7 +83,8 @@ export class ReadarrIntegration extends MediaOrganizerIntegration { const flatImages = [...event.images]; const sortedImages = flatImages.sort( - (imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType), + (imageA, imageB) => + mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType), ); logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`); return sortedImages[0]; diff --git a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts index 0c0482130..e82ff9094 100644 --- a/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts +++ b/packages/integrations/src/media-organizer/sonarr/sonarr-integration.ts @@ -3,13 +3,15 @@ import { z } from "zod"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { logger } from "@homarr/log"; +import { Integration } from "../../base/integration"; import type { IntegrationTestingInput } from "../../base/integration"; import { TestConnectionError } from "../../base/test-connection/test-connection-error"; import type { TestingResult } from "../../base/test-connection/test-connection-service"; -import type { CalendarEvent } from "../../calendar-types"; -import { MediaOrganizerIntegration } from "../media-organizer-integration"; +import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; +import { mediaOrganizerPriorities } from "../media-organizer"; -export class SonarrIntegration extends MediaOrganizerIntegration { +export class SonarrIntegration extends Integration implements ICalendarIntegration { /** * Gets the events in the Sonarr calendar between two dates. * @param start The start date @@ -81,7 +83,8 @@ export class SonarrIntegration extends MediaOrganizerIntegration { const flatImages = [...event.images, ...event.series.images]; const sortedImages = flatImages.sort( - (imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType), + (imageA, imageB) => + mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType), ); logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`); return sortedImages[0]; diff --git a/packages/integrations/src/media-transcoding/tdarr-integration.ts b/packages/integrations/src/media-transcoding/tdarr-integration.ts index 68e24336c..fc3208f5f 100644 --- a/packages/integrations/src/media-transcoding/tdarr-integration.ts +++ b/packages/integrations/src/media-transcoding/tdarr-integration.ts @@ -4,12 +4,11 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { TdarrQueue } from "../interfaces/media-transcoding/queue"; -import type { TdarrStatistics } from "../interfaces/media-transcoding/statistics"; -import type { TdarrWorker } from "../interfaces/media-transcoding/workers"; +import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration"; +import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "../interfaces/media-transcoding/media-transcoding-types"; import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } from "./tdarr-validation-schemas"; -export class TdarrIntegration extends Integration { +export class TdarrIntegration extends Integration implements IMediaTranscodingIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const response = await input.fetchAsync(this.url("/api/v2/is-server-alive"), { method: "POST", diff --git a/packages/integrations/src/mock/data/calendar.ts b/packages/integrations/src/mock/data/calendar.ts new file mode 100644 index 000000000..efc8576d7 --- /dev/null +++ b/packages/integrations/src/mock/data/calendar.ts @@ -0,0 +1,74 @@ +import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../../interfaces/calendar/calendar-types"; + +export class CalendarMockService implements ICalendarIntegration { + public async getCalendarEventsAsync(start: Date, end: Date, _includeUnmonitored: boolean): Promise { + const result = [homarrMeetup(start, end), titanicRelease(start, end), seriesRelease(start, end)]; + return await Promise.resolve(result); + } +} + +const homarrMeetup = (start: Date, end: Date): CalendarEvent => ({ + name: "Homarr Meetup", + subName: "", + description: "Yearly meetup of the Homarr community", + date: randomDateBetween(start, end), + links: [ + { + href: "https://homarr.dev", + name: "Homarr", + logo: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/homarr.svg", + color: "#000000", + notificationColor: "#fa5252", + isDark: true, + }, + ], +}); + +const titanicRelease = (start: Date, end: Date): CalendarEvent => ({ + name: "Titanic", + subName: "A classic movie", + description: "A tragic love story set on the ill-fated RMS Titanic.", + date: randomDateBetween(start, end), + thumbnail: "https://image.tmdb.org/t/p/original/5bTWA20cL9LCIGNpde4Epc2Ijzn.jpg", + mediaInformation: { + type: "movie", + }, + links: [ + { + href: "https://www.imdb.com/title/tt0120338/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + notificationColor: "cyan", + }, + ], +}); + +const seriesRelease = (start: Date, end: Date): CalendarEvent => ({ + name: "The Mandalorian", + subName: "A Star Wars Series", + description: "A lone bounty hunter in the outer reaches of the galaxy.", + date: randomDateBetween(start, end), + thumbnail: "https://image.tmdb.org/t/p/original/ztvm7C7hiUpS3CZRXFmJxljICzK.jpg", + mediaInformation: { + type: "tv", + seasonNumber: 1, + episodeNumber: 1, + }, + links: [ + { + href: "https://www.imdb.com/title/tt8111088/", + name: "IMDb", + color: "#f5c518", + isDark: false, + logo: "/images/apps/imdb.svg", + notificationColor: "blue", + }, + ], +}); + +function randomDateBetween(start: Date, end: Date): Date { + return new Date(start.getTime() + Math.random() * (end.getTime() - start.getTime())); +} diff --git a/packages/integrations/src/mock/data/cluster-health-monitoring.ts b/packages/integrations/src/mock/data/cluster-health-monitoring.ts new file mode 100644 index 000000000..00a8a8c7d --- /dev/null +++ b/packages/integrations/src/mock/data/cluster-health-monitoring.ts @@ -0,0 +1,100 @@ +import type { IClusterHealthMonitoringIntegration } from "../../interfaces/health-monitoring/health-monitoring-integration"; +import type { ClusterHealthMonitoring } from "../../types"; + +export class ClusterHealthMonitoringMockService implements IClusterHealthMonitoringIntegration { + public async getClusterInfoAsync(): Promise { + return Promise.resolve({ + nodes: Array.from({ length: 5 }, (_, index) => ClusterHealthMonitoringMockService.createNode(index)), + lxcs: Array.from({ length: 3 }, (_, index) => ClusterHealthMonitoringMockService.createLxc(index)), + vms: Array.from({ length: 7 }, (_, index) => ClusterHealthMonitoringMockService.createVm(index)), + storages: Array.from({ length: 9 }, (_, index) => ClusterHealthMonitoringMockService.createStorage(index)), + }); + } + + private static createNode(index: number): ClusterHealthMonitoring["nodes"][number] { + return { + id: index.toString(), + name: `Node ${index}`, + isRunning: Math.random() > 0.1, // 90% chance of being running + node: `Node ${index}`, + status: Math.random() > 0.5 ? "online" : "offline", + type: "node", + uptime: Math.floor(Math.random() * 1000000), // Randomly generate uptime in seconds + haState: null, + ...this.createResourceUsage(), + }; + } + + private static createResourceUsage() { + const totalMemory = Math.pow(2, Math.floor(Math.random() * 6) + 1) * 1024 * 1024 * 1024; // Randomly generate between 2GB and 64GB + const totalStorage = Math.pow(2, Math.floor(Math.random() * 6) + 1) * 1024 * 1024 * 1024; // Randomly generate between 2GB and 64GB + + return { + cpu: { + cores: Math.pow(2, Math.floor(Math.random() * 5) + 1), // Randomly generate between 2 and 32 cores, + utilization: Math.random(), + }, + memory: { + total: totalMemory, + used: Math.floor(Math.random() * totalMemory), // Randomly generate used memory + }, + network: { + in: Math.floor(Math.random() * 1000), // Randomly generate network in + out: Math.floor(Math.random() * 1000), // Randomly generate network out + }, + storage: { + total: totalStorage, + used: Math.floor(Math.random() * totalStorage), // Randomly generate used storage + read: Math.floor(Math.random() * 1000), // Randomly generate read + write: Math.floor(Math.random() * 1000), // Randomly generate write + }, + }; + } + + private static createVm(index: number): ClusterHealthMonitoring["vms"][number] { + return { + id: index.toString(), + name: `VM ${index}`, + vmId: index + 1000, // VM IDs start from 1000 + ...this.createResourceUsage(), + haState: null, + isRunning: Math.random() > 0.1, // 90% chance of being running + node: `Node ${Math.floor(index / 2)}`, // Assign to a node + status: Math.random() > 0.5 ? "online" : "offline", + type: "qemu", + uptime: Math.floor(Math.random() * 1000000), // Randomly generate uptime in seconds + }; + } + + private static createLxc(index: number): ClusterHealthMonitoring["lxcs"][number] { + return { + id: index.toString(), + name: `LXC ${index}`, + vmId: index + 2000, // LXC IDs start from 2000 + ...this.createResourceUsage(), + haState: null, + isRunning: Math.random() > 0.1, // 90% chance of being running + node: `Node ${Math.floor(index / 2)}`, // Assign to a node + status: Math.random() > 0.5 ? "online" : "offline", + type: "lxc", + uptime: Math.floor(Math.random() * 1000000), // Randomly generate uptime in seconds + }; + } + + private static createStorage(index: number): ClusterHealthMonitoring["storages"][number] { + const total = Math.pow(2, Math.floor(Math.random() * 6) + 1) * 1024 * 1024 * 1024; // Randomly generate between 2GB and 64GB + + return { + id: index.toString(), + name: `Storage ${index}`, + isRunning: Math.random() > 0.1, // 90% chance of being running + node: `Node ${Math.floor(index / 2)}`, // Assign to a node + status: Math.random() > 0.5 ? "online" : "offline", + isShared: Math.random() > 0.5, // 50% chance of being shared + storagePlugin: `Plugin ${index}`, + total, + used: Math.floor(Math.random() * total), // Randomly generate used storage + type: "storage", + }; + } +} diff --git a/packages/integrations/src/mock/data/dns-hole.ts b/packages/integrations/src/mock/data/dns-hole.ts new file mode 100644 index 000000000..c5dcf886b --- /dev/null +++ b/packages/integrations/src/mock/data/dns-hole.ts @@ -0,0 +1,26 @@ +import type { DnsHoleSummaryIntegration } from "../../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { DnsHoleSummary } from "../../types"; + +export class DnsHoleMockService implements DnsHoleSummaryIntegration { + private static isEnabled = true; + + public async getSummaryAsync(): Promise { + const blocked = Math.floor(Math.random() * Math.pow(10, 4)) + 1; // Ensure we never devide by zero + const queries = Math.max(Math.floor(Math.random() * Math.pow(10, 5)), blocked); + return await Promise.resolve({ + status: DnsHoleMockService.isEnabled ? "enabled" : "disabled", + domainsBeingBlocked: Math.floor(Math.random() * Math.pow(10, 6)), + adsBlockedToday: blocked, + adsBlockedTodayPercentage: blocked / queries, + dnsQueriesToday: queries, + }); + } + public async enableAsync(): Promise { + DnsHoleMockService.isEnabled = true; + return await Promise.resolve(); + } + public async disableAsync(_duration?: number): Promise { + DnsHoleMockService.isEnabled = false; + return await Promise.resolve(); + } +} diff --git a/packages/integrations/src/mock/data/download.ts b/packages/integrations/src/mock/data/download.ts new file mode 100644 index 000000000..7d3ab0252 --- /dev/null +++ b/packages/integrations/src/mock/data/download.ts @@ -0,0 +1,58 @@ +import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data"; +import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration"; +import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items"; + +export class DownloadClientMockService implements IDownloadClientIntegration { + public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { + return await Promise.resolve({ + status: { + paused: Math.random() < 0.5, + rates: { + down: Math.floor(Math.random() * 5000), + up: Math.floor(Math.random() * 5000), + }, + types: ["torrent", "usenet"], + }, + items: Array.from({ length: 20 }, (_, index) => DownloadClientMockService.createItem(index)).slice( + 0, + input.limit, + ), + }); + } + + public async pauseQueueAsync(): Promise { + return await Promise.resolve(); + } + + public async pauseItemAsync(_item: DownloadClientItem): Promise { + return await Promise.resolve(); + } + + public async resumeQueueAsync(): Promise { + return Promise.resolve(); + } + + public async resumeItemAsync(_item: DownloadClientItem): Promise { + return await Promise.resolve(); + } + + public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise { + return await Promise.resolve(); + } + + private static createItem(index: number): DownloadClientItem { + const progress = Math.random() < 0.5 ? Math.random() : 1; + return { + id: `item-${index}`, + index, + name: `Item ${index}`, + type: Math.random() > 0.5 ? "torrent" : "usenet", + progress, + size: Math.floor(Math.random() * 10000) + 1, + downSpeed: Math.floor(Math.random() * 5000), + upSpeed: Math.floor(Math.random() * 5000), + state: progress >= 1 ? "completed" : "downloading", + time: 0, + }; + } +} diff --git a/packages/integrations/src/mock/data/indexer-manager.ts b/packages/integrations/src/mock/data/indexer-manager.ts new file mode 100644 index 000000000..b0883c518 --- /dev/null +++ b/packages/integrations/src/mock/data/indexer-manager.ts @@ -0,0 +1,23 @@ +import type { IIndexerManagerIntegration } from "../../interfaces/indexer-manager/indexer-manager-integration"; +import type { Indexer } from "../../types"; + +export class IndexerManagerMockService implements IIndexerManagerIntegration { + public async getIndexersAsync(): Promise { + return await Promise.resolve( + Array.from({ length: 10 }, (_, index) => IndexerManagerMockService.createIndexer(index + 1)), + ); + } + public async testAllAsync(): Promise { + await Promise.resolve(); + } + + private static createIndexer(index: number): Indexer { + return { + id: index, + name: `Mock Indexer ${index}`, + url: `https://mock-indexer-${index}.com`, + enabled: Math.random() > 0.2, // 80% chance of being enabled + status: Math.random() > 0.2, // 80% chance of being active + }; + } +} diff --git a/packages/integrations/src/mock/data/media-request.ts b/packages/integrations/src/mock/data/media-request.ts new file mode 100644 index 000000000..5137177d8 --- /dev/null +++ b/packages/integrations/src/mock/data/media-request.ts @@ -0,0 +1,97 @@ +import { objectEntries } from "@homarr/common"; + +import type { IMediaRequestIntegration } from "../../interfaces/media-requests/media-request-integration"; +import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "../../types"; +import { MediaAvailability, MediaRequestStatus } from "../../types"; + +export class MediaRequestMockService implements IMediaRequestIntegration { + public async getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise { + return await Promise.resolve({ + id, + overview: `Overview of media ${id}`, + posterPath: "https://image.tmdb.org/t/p/original/ztvm7C7hiUpS3CZRXFmJxljICzK.jpg", + seasons: + mediaType === "tv" + ? Array.from({ length: 3 }, (_, seasonIndex) => ({ + id: seasonIndex + 1, + name: `Season ${seasonIndex + 1}`, + episodeCount: Math.floor(Math.random() * 10) + 1, + overview: `Overview of season ${seasonIndex + 1} of media ${id}`, + })) + : undefined, + }); + } + public async requestMediaAsync(_mediaType: "movie" | "tv", _id: number, _seasons?: number[]): Promise { + await Promise.resolve(); + } + public async getRequestsAsync(): Promise { + const result = await Promise.resolve( + Array.from({ length: 10 }, (_, index) => MediaRequestMockService.createRequest(index)), + ); + + return result; + } + public async getStatsAsync(): Promise { + return await Promise.resolve({ + approved: Math.floor(Math.random() * 100), + available: Math.floor(Math.random() * 100), + declined: Math.floor(Math.random() * 100), + movie: Math.floor(Math.random() * 100), + pending: Math.floor(Math.random() * 100), + processing: Math.floor(Math.random() * 100), + total: Math.floor(Math.random() * 1000), + tv: Math.floor(Math.random() * 100), + }); + } + public async getUsersAsync(): Promise { + return await Promise.resolve(Array.from({ length: 5 }, (_, index) => MediaRequestMockService.createUser(index))); + } + + public async approveRequestAsync(_requestId: number): Promise { + await Promise.resolve(); + } + public async declineRequestAsync(_requestId: number): Promise { + await Promise.resolve(); + } + + private static createUser(index: number): RequestUser { + return { + id: index, + displayName: `User ${index}`, + avatar: "/images/mock/avatar.jpg", + requestCount: Math.floor(Math.random() * 100), + link: `https://example.com/user/${index}`, + }; + } + + private static createRequest(index: number): MediaRequest { + return { + id: index, + name: `Media Request ${index}`, + availability: this.randomAvailability(), + backdropImageUrl: "https://image.tmdb.org/t/p/original/ztvm7C7hiUpS3CZRXFmJxljICzK.jpg", + posterImagePath: "https://image.tmdb.org/t/p/original/ztvm7C7hiUpS3CZRXFmJxljICzK.jpg", + createdAt: new Date(), + airDate: new Date(Date.now() + (Math.random() - 0.5) * 1000 * 60 * 60 * 24 * 365 * 4), + status: this.randomStatus(), + href: `https://example.com/media/${index}`, + type: Math.random() > 0.5 ? "movie" : "tv", + requestedBy: { + avatar: "/images/mock/avatar.jpg", + displayName: `User ${index}`, + id: index, + link: `https://example.com/user/${index}`, + }, + }; + } + + private static randomAvailability(): MediaAvailability { + const values = objectEntries(MediaAvailability).filter(([key]) => typeof key === "number"); + return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaAvailability.Available; + } + + private static randomStatus(): MediaRequestStatus { + const values = objectEntries(MediaRequestStatus).filter(([key]) => typeof key === "number"); + return values[Math.floor(Math.random() * values.length)]?.[1] ?? MediaRequestStatus.PendingApproval; + } +} diff --git a/packages/integrations/src/mock/data/media-server.ts b/packages/integrations/src/mock/data/media-server.ts new file mode 100644 index 000000000..6a3211b63 --- /dev/null +++ b/packages/integrations/src/mock/data/media-server.ts @@ -0,0 +1,35 @@ +import type { IMediaServerIntegration } from "../../interfaces/media-server/media-server-integration"; +import type { CurrentSessionsInput, StreamSession } from "../../interfaces/media-server/media-server-types"; + +export class MediaServerMockService implements IMediaServerIntegration { + public async getCurrentSessionsAsync(options: CurrentSessionsInput): Promise { + return await Promise.resolve( + Array.from({ length: 10 }, (_, index) => MediaServerMockService.createSession(index)).filter( + (session) => !options.showOnlyPlaying || session.currentlyPlaying !== null, + ), + ); + } + + private static createSession(index: number): StreamSession { + return { + sessionId: `session-${index}`, + sessionName: `Session ${index}`, + user: { + userId: `user-${index}`, + username: `User${index}`, + profilePictureUrl: "/images/mock/avatar.jpg", + }, + currentlyPlaying: + Math.random() > 0.9 // 10% chance of being null (not currently playing) + ? { + type: "movie", + name: `Movie ${index}`, + seasonName: undefined, + episodeName: null, + albumName: null, + episodeCount: null, + } + : null, + }; + } +} diff --git a/packages/integrations/src/mock/data/media-transcoding.ts b/packages/integrations/src/mock/data/media-transcoding.ts new file mode 100644 index 000000000..32e94bdb7 --- /dev/null +++ b/packages/integrations/src/mock/data/media-transcoding.ts @@ -0,0 +1,68 @@ +import type { IMediaTranscodingIntegration } from "../../interfaces/media-transcoding/media-transcoding-integration"; +import type { + TdarrQueue, + TdarrStatistics, + TdarrWorker, +} from "../../interfaces/media-transcoding/media-transcoding-types"; + +export class MediaTranscodingMockService implements IMediaTranscodingIntegration { + public async getStatisticsAsync(): Promise { + return await Promise.resolve({ + libraryName: "Mock Library", + totalFileCount: 1000, + totalTranscodeCount: 200, + totalHealthCheckCount: 150, + failedTranscodeCount: 10, + failedHealthCheckCount: 5, + stagedTranscodeCount: 20, + stagedHealthCheckCount: 15, + totalSavedSpace: 5000000, + audioCodecs: [{ name: "AAC", value: 300 }], + audioContainers: [{ name: "MP4", value: 200 }], + videoCodecs: [{ name: "H.264", value: 400 }], + videoContainers: [{ name: "MKV", value: 250 }], + videoResolutions: [{ name: "1080p", value: 600 }], + healthCheckStatus: [{ name: "Healthy", value: 100 }], + transcodeStatus: [{ name: "Transcode success", value: 180 }], + }); + } + public async getWorkersAsync(): Promise { + return await Promise.resolve( + Array.from({ length: 5 }, (_, index) => MediaTranscodingMockService.createWorker(index)), + ); + } + public async getQueueAsync(firstItemIndex: number, pageSize: number): Promise { + return await Promise.resolve({ + array: Array.from({ length: pageSize }, (_, index) => ({ + id: `item-${firstItemIndex + index}`, + healthCheck: "Pending", + transcode: "Pending", + filePath: `/path/to/file-${firstItemIndex + index}.mkv`, + fileSize: 1000000000 + (firstItemIndex + index) * 100000000, // in bytes + container: "MKV", + codec: "H.264", + resolution: "1080p", + type: "transcode", + })), + totalCount: 50, + startIndex: firstItemIndex, + endIndex: firstItemIndex + pageSize - 1, + }); + } + + private static createWorker(index: number): TdarrWorker { + return { + id: `worker-${index}`, + filePath: `/path/to/file-${index}.mkv`, + fps: 24 + index, + percentage: index * 20, + ETA: `${30 - index * 5} minutes`, + jobType: "Transcode", + status: "In Progress", + step: `Step ${index + 1}`, + originalSize: 1000000000 + index * 100000000, // in bytes + estimatedSize: 800000000 + index * 50000000, // in bytes + outputSize: 750000000 + index * 40000000, // in bytes + }; + } +} diff --git a/packages/integrations/src/mock/data/network-controller-summary.ts b/packages/integrations/src/mock/data/network-controller-summary.ts new file mode 100644 index 000000000..7ffe80c71 --- /dev/null +++ b/packages/integrations/src/mock/data/network-controller-summary.ts @@ -0,0 +1,30 @@ +import type { NetworkControllerSummaryIntegration } from "../../interfaces/network-controller-summary/network-controller-summary-integration"; +import type { NetworkControllerSummary } from "../../types"; + +export class NetworkControllerSummaryMockService implements NetworkControllerSummaryIntegration { + public async getNetworkSummaryAsync(): Promise { + return await Promise.resolve({ + lan: { + guests: 5, + users: 10, + status: "enabled", + }, + vpn: { + users: 3, + status: "enabled", + }, + wanStatus: "disabled", + wifi: { + status: "disabled", + guests: 0, + users: 0, + }, + www: { + latency: 22, + status: "enabled", + ping: 32, + uptime: 3600, + }, + }); + } +} diff --git a/packages/integrations/src/mock/data/notifications.ts b/packages/integrations/src/mock/data/notifications.ts new file mode 100644 index 000000000..a4ef0d08f --- /dev/null +++ b/packages/integrations/src/mock/data/notifications.ts @@ -0,0 +1,19 @@ +import type { Notification } from "../../interfaces/notifications/notification-types"; +import type { INotificationsIntegration } from "../../interfaces/notifications/notifications-integration"; + +export class NotificationsMockService implements INotificationsIntegration { + public async getNotificationsAsync(): Promise { + return await Promise.resolve( + Array.from({ length: 10 }, (_, index) => NotificationsMockService.createNotification(index)), + ); + } + + private static createNotification(index: number): Notification { + return { + id: index.toString(), + time: new Date(Date.now() - Math.random() * 1000000), // Random time within the next 11 days + title: `Notification ${index}`, + body: `This is the body of notification ${index}.`, + }; + } +} diff --git a/packages/integrations/src/mock/data/smart-home.ts b/packages/integrations/src/mock/data/smart-home.ts new file mode 100644 index 000000000..0556c28d8 --- /dev/null +++ b/packages/integrations/src/mock/data/smart-home.ts @@ -0,0 +1,27 @@ +import type { ISmartHomeIntegration } from "../../interfaces/smart-home/smart-home-integration"; +import type { EntityStateResult } from "../../interfaces/smart-home/smart-home-types"; + +export class SmartHomeMockService implements ISmartHomeIntegration { + public async getEntityStateAsync(entityId: string): Promise { + return await Promise.resolve({ + success: true as const, + data: { + entity_id: entityId, + state: "on", + attributes: { + friendly_name: `Mock Entity ${entityId}`, + device_class: "light", + supported_features: 1, + }, + last_changed: new Date(), + last_updated: new Date(), + }, + }); + } + public async triggerAutomationAsync(_entityId: string): Promise { + return await Promise.resolve(true); + } + public async triggerToggleAsync(_entityId: string): Promise { + return await Promise.resolve(true); + } +} diff --git a/packages/integrations/src/mock/data/system-health-monitoring.ts b/packages/integrations/src/mock/data/system-health-monitoring.ts new file mode 100644 index 000000000..6ff1bb23c --- /dev/null +++ b/packages/integrations/src/mock/data/system-health-monitoring.ts @@ -0,0 +1,36 @@ +import type { SystemHealthMonitoring } from "../.."; +import type { ISystemHealthMonitoringIntegration } from "../../interfaces/health-monitoring/health-monitoring-integration"; + +export class SystemHealthMonitoringMockService implements ISystemHealthMonitoringIntegration { + public async getSystemInfoAsync(): Promise { + return await Promise.resolve({ + version: "1.0.0", + cpuModelName: "Mock CPU", + cpuUtilization: Math.random(), + memUsed: (4 * 1024 * 1024 * 1024).toString(), // 4 GB in bytes + memAvailable: (8 * 1024 * 1024 * 1024).toString(), // 8 GB in bytes + availablePkgUpdates: 0, + rebootRequired: false, + cpuTemp: Math.floor(Math.random() * 100), // Random temperature between 0 and 99 + uptime: Math.floor(Math.random() * 1000000), // Random uptime in seconds + fileSystem: Array.from({ length: 3 }, (_, index) => ({ + deviceName: `sha${index + 1}`, + used: "1 GB", + available: "500 MB", + percentage: Math.floor(Math.random() * 100), // Random percentage between 0 and 99 + })), + loadAverage: { + "1min": Math.random() * 10, + "5min": Math.random() * 10, + "15min": Math.random() * 10, + }, + smart: [ + { + deviceName: "Mock Device", + temperature: Math.floor(Math.random() * 100), // Random temperature between 0 and 99 + overallStatus: "OK", + }, + ], + }); + } +} diff --git a/packages/integrations/src/mock/mock-integration.ts b/packages/integrations/src/mock/mock-integration.ts new file mode 100644 index 000000000..c3fc71c4d --- /dev/null +++ b/packages/integrations/src/mock/mock-integration.ts @@ -0,0 +1,119 @@ +import type { IntegrationTestingInput } from "../base/integration"; +import { Integration } from "../base/integration"; +import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration"; +import type { DnsHoleSummaryIntegration } from "../interfaces/dns-hole-summary/dns-hole-summary-integration"; +import type { IDownloadClientIntegration } from "../interfaces/downloads/download-client-integration"; +import type { + IClusterHealthMonitoringIntegration, + ISystemHealthMonitoringIntegration, +} from "../interfaces/health-monitoring/health-monitoring-integration"; +import type { IIndexerManagerIntegration } from "../interfaces/indexer-manager/indexer-manager-integration"; +import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration"; +import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; +import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration"; +import type { NetworkControllerSummaryIntegration } from "../interfaces/network-controller-summary/network-controller-summary-integration"; +import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration"; +import { CalendarMockService } from "./data/calendar"; +import { ClusterHealthMonitoringMockService } from "./data/cluster-health-monitoring"; +import { DnsHoleMockService } from "./data/dns-hole"; +import { DownloadClientMockService } from "./data/download"; +import { IndexerManagerMockService } from "./data/indexer-manager"; +import { MediaRequestMockService } from "./data/media-request"; +import { MediaServerMockService } from "./data/media-server"; +import { MediaTranscodingMockService } from "./data/media-transcoding"; +import { NetworkControllerSummaryMockService } from "./data/network-controller-summary"; +import { NotificationsMockService } from "./data/notifications"; +import { SmartHomeMockService } from "./data/smart-home"; +import { SystemHealthMonitoringMockService } from "./data/system-health-monitoring"; + +export class MockIntegration + extends Integration + implements + DnsHoleSummaryIntegration, + ICalendarIntegration, + IDownloadClientIntegration, + IClusterHealthMonitoringIntegration, + ISystemHealthMonitoringIntegration, + IIndexerManagerIntegration, + IMediaRequestIntegration, + IMediaServerIntegration, + IMediaTranscodingIntegration, + NetworkControllerSummaryIntegration, + ISmartHomeIntegration +{ + private static readonly dnsHole = new DnsHoleMockService(); + private static readonly calendar = new CalendarMockService(); + private static readonly downloadClient = new DownloadClientMockService(); + private static readonly clusterMonitoring = new ClusterHealthMonitoringMockService(); + private static readonly systemMonitoring = new SystemHealthMonitoringMockService(); + private static readonly indexerManager = new IndexerManagerMockService(); + private static readonly mediaRequest = new MediaRequestMockService(); + private static readonly mediaServer = new MediaServerMockService(); + private static readonly mediaTranscoding = new MediaTranscodingMockService(); + private static readonly networkController = new NetworkControllerSummaryMockService(); + private static readonly notifications = new NotificationsMockService(); + private static readonly smartHome = new SmartHomeMockService(); + + protected async testingAsync(_: IntegrationTestingInput): Promise { + return await Promise.resolve({ + success: true, + }); + } + + // CalendarIntegration + getCalendarEventsAsync = MockIntegration.calendar.getCalendarEventsAsync.bind(MockIntegration.calendar); + + // DnsHoleSummaryIntegration + getSummaryAsync = MockIntegration.dnsHole.getSummaryAsync.bind(MockIntegration.dnsHole); + enableAsync = MockIntegration.dnsHole.enableAsync.bind(MockIntegration.dnsHole); + disableAsync = MockIntegration.dnsHole.disableAsync.bind(MockIntegration.dnsHole); + + // IDownloadClientIntegration + getClientJobsAndStatusAsync = MockIntegration.downloadClient.getClientJobsAndStatusAsync.bind( + MockIntegration.downloadClient, + ); + pauseQueueAsync = MockIntegration.downloadClient.pauseQueueAsync.bind(MockIntegration.downloadClient); + pauseItemAsync = MockIntegration.downloadClient.pauseItemAsync.bind(MockIntegration.downloadClient); + resumeQueueAsync = MockIntegration.downloadClient.resumeQueueAsync.bind(MockIntegration.downloadClient); + resumeItemAsync = MockIntegration.downloadClient.resumeItemAsync.bind(MockIntegration.downloadClient); + deleteItemAsync = MockIntegration.downloadClient.deleteItemAsync.bind(MockIntegration.downloadClient); + + // Health Monitoring Integrations + getSystemInfoAsync = MockIntegration.systemMonitoring.getSystemInfoAsync.bind(MockIntegration.systemMonitoring); + getClusterInfoAsync = MockIntegration.clusterMonitoring.getClusterInfoAsync.bind(MockIntegration.downloadClient); + + // IndexerManagerIntegration + getIndexersAsync = MockIntegration.indexerManager.getIndexersAsync.bind(MockIntegration.indexerManager); + testAllAsync = MockIntegration.indexerManager.testAllAsync.bind(MockIntegration.indexerManager); + + // MediaRequestIntegration + getSeriesInformationAsync = MockIntegration.mediaRequest.getSeriesInformationAsync.bind(MockIntegration.mediaRequest); + requestMediaAsync = MockIntegration.mediaRequest.requestMediaAsync.bind(MockIntegration.mediaRequest); + getRequestsAsync = MockIntegration.mediaRequest.getRequestsAsync.bind(MockIntegration.mediaRequest); + getStatsAsync = MockIntegration.mediaRequest.getStatsAsync.bind(MockIntegration.mediaRequest); + getUsersAsync = MockIntegration.mediaRequest.getUsersAsync.bind(MockIntegration.mediaRequest); + approveRequestAsync = MockIntegration.mediaRequest.approveRequestAsync.bind(MockIntegration.mediaRequest); + declineRequestAsync = MockIntegration.mediaRequest.declineRequestAsync.bind(MockIntegration.mediaRequest); + + // MediaServerIntegration + getCurrentSessionsAsync = MockIntegration.mediaServer.getCurrentSessionsAsync.bind(MockIntegration.mediaRequest); + + // MediaTranscodingIntegration + getStatisticsAsync = MockIntegration.mediaTranscoding.getStatisticsAsync.bind(MockIntegration.mediaTranscoding); + getWorkersAsync = MockIntegration.mediaTranscoding.getWorkersAsync.bind(MockIntegration.mediaTranscoding); + getQueueAsync = MockIntegration.mediaTranscoding.getQueueAsync.bind(MockIntegration.mediaTranscoding); + + // NetworkControllerSummaryIntegration + getNetworkSummaryAsync = MockIntegration.networkController.getNetworkSummaryAsync.bind( + MockIntegration.networkController, + ); + + // NotificationsIntegration + getNotificationsAsync = MockIntegration.notifications.getNotificationsAsync.bind(MockIntegration.notifications); + + // SmartHomeIntegration + getEntityStateAsync = MockIntegration.smartHome.getEntityStateAsync.bind(MockIntegration.smartHome); + triggerAutomationAsync = MockIntegration.smartHome.triggerAutomationAsync.bind(MockIntegration.smartHome); + triggerToggleAsync = MockIntegration.smartHome.triggerToggleAsync.bind(MockIntegration.smartHome); +} diff --git a/packages/integrations/src/nextcloud/nextcloud.integration.ts b/packages/integrations/src/nextcloud/nextcloud.integration.ts index f6c1919dc..4a9e5d3f5 100644 --- a/packages/integrations/src/nextcloud/nextcloud.integration.ts +++ b/packages/integrations/src/nextcloud/nextcloud.integration.ts @@ -10,10 +10,11 @@ import { integrationTsdavHttpErrorHandler } from "../base/errors/http"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { CalendarEvent } from "../calendar-types"; +import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration"; +import type { CalendarEvent } from "../interfaces/calendar/calendar-types"; @HandleIntegrationErrors([integrationTsdavHttpErrorHandler]) -export class NextcloudIntegration extends Integration { +export class NextcloudIntegration extends Integration implements ICalendarIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const client = await this.createCalendarClientAsync(input.dispatcher); await client.login(); diff --git a/packages/integrations/src/ntfy/ntfy-integration.ts b/packages/integrations/src/ntfy/ntfy-integration.ts index ffdf9dfad..1c8dfa31b 100644 --- a/packages/integrations/src/ntfy/ntfy-integration.ts +++ b/packages/integrations/src/ntfy/ntfy-integration.ts @@ -1,13 +1,14 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; import { ResponseError } from "@homarr/common/server"; +import { Integration } from "../base/integration"; import type { IntegrationTestingInput } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { Notification } from "../interfaces/notifications/notification"; -import { NotificationsIntegration } from "../interfaces/notifications/notifications-integration"; +import type { Notification } from "../interfaces/notifications/notification-types"; +import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration"; import { ntfyNotificationSchema } from "./ntfy-schema"; -export class NTFYIntegration extends NotificationsIntegration { +export class NTFYIntegration extends Integration implements INotificationsIntegration { public async testingAsync(input: IntegrationTestingInput): Promise { await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() }); return { success: true }; diff --git a/packages/integrations/src/openmediavault/openmediavault-integration.ts b/packages/integrations/src/openmediavault/openmediavault-integration.ts index e03b0ae07..3e2fdd69b 100644 --- a/packages/integrations/src/openmediavault/openmediavault-integration.ts +++ b/packages/integrations/src/openmediavault/openmediavault-integration.ts @@ -9,7 +9,8 @@ import { Integration } from "../base/integration"; import type { SessionStore } from "../base/session-store"; import { createSessionStore } from "../base/session-store"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { HealthMonitoring } from "../types"; +import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; +import type { SystemHealthMonitoring } from "../types"; import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types"; const localLogger = logger.child({ module: "OpenMediaVaultIntegration" }); @@ -18,7 +19,7 @@ type SessionStoreValue = | { type: "header"; sessionId: string } | { type: "cookie"; loginToken: string; sessionId: string }; -export class OpenMediaVaultIntegration extends Integration { +export class OpenMediaVaultIntegration extends Integration implements ISystemHealthMonitoringIntegration { private readonly sessionStore: SessionStore; constructor(integration: IntegrationInput) { @@ -26,7 +27,7 @@ export class OpenMediaVaultIntegration extends Integration { this.sessionStore = createSessionStore(integration); } - public async getSystemInfoAsync(): Promise { + public async getSystemInfoAsync(): Promise { const systemResponses = await this.makeAuthenticatedRpcCallAsync("system", "getInformation"); const fileSystemResponse = await this.makeAuthenticatedRpcCallAsync( "filesystemmgmt", diff --git a/packages/integrations/src/overseerr/overseerr-integration.ts b/packages/integrations/src/overseerr/overseerr-integration.ts index 8296a7a09..66c1841fc 100644 --- a/packages/integrations/src/overseerr/overseerr-integration.ts +++ b/packages/integrations/src/overseerr/overseerr-integration.ts @@ -8,8 +8,9 @@ import { Integration } from "../base/integration"; import type { ISearchableIntegration } from "../base/searchable-integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request"; -import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request"; +import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration"; +import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types"; +import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types"; interface OverseerrSearchResult { id: number; @@ -23,7 +24,10 @@ interface OverseerrSearchResult { /** * Overseerr Integration. See https://api-docs.overseerr.dev */ -export class OverseerrIntegration extends Integration implements ISearchableIntegration { +export class OverseerrIntegration + extends Integration + implements IMediaRequestIntegration, ISearchableIntegration +{ public async searchAsync(query: string) { const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), { headers: { diff --git a/packages/integrations/src/plex/plex-integration.ts b/packages/integrations/src/plex/plex-integration.ts index 4cdb61984..1025d9df2 100644 --- a/packages/integrations/src/plex/plex-integration.ts +++ b/packages/integrations/src/plex/plex-integration.ts @@ -8,10 +8,11 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session"; +import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration"; +import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types"; import type { PlexResponse } from "./interface"; -export class PlexIntegration extends Integration { +export class PlexIntegration extends Integration implements IMediaServerIntegration { public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise { const token = super.getSecretValue("apiKey"); diff --git a/packages/integrations/src/prowlarr/prowlarr-integration.ts b/packages/integrations/src/prowlarr/prowlarr-integration.ts index 992d13986..729d41f41 100644 --- a/packages/integrations/src/prowlarr/prowlarr-integration.ts +++ b/packages/integrations/src/prowlarr/prowlarr-integration.ts @@ -6,10 +6,11 @@ import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import { TestConnectionError } from "../base/test-connection/test-connection-error"; import type { TestingResult } from "../base/test-connection/test-connection-service"; -import type { Indexer } from "../interfaces/indexer-manager/indexer"; +import type { IIndexerManagerIntegration } from "../interfaces/indexer-manager/indexer-manager-integration"; +import type { Indexer } from "../interfaces/indexer-manager/indexer-manager-types"; import { indexerResponseSchema, statusResponseSchema } from "./prowlarr-types"; -export class ProwlarrIntegration extends Integration { +export class ProwlarrIntegration extends Integration implements IIndexerManagerIntegration { public async getIndexersAsync(): Promise { const apiKey = super.getSecretValue("apiKey"); diff --git a/packages/integrations/src/proxmox/proxmox-integration.ts b/packages/integrations/src/proxmox/proxmox-integration.ts index cdf6c8064..6ae65feab 100644 --- a/packages/integrations/src/proxmox/proxmox-integration.ts +++ b/packages/integrations/src/proxmox/proxmox-integration.ts @@ -8,6 +8,7 @@ import { HandleIntegrationErrors } from "../base/errors/decorator"; import type { IntegrationTestingInput } from "../base/integration"; import { Integration } from "../base/integration"; import type { TestingResult } from "../base/test-connection/test-connection-service"; +import type { IClusterHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration"; import { ProxmoxApiErrorHandler } from "./proxmox-error-handler"; import type { ComputeResourceBase, @@ -19,7 +20,7 @@ import type { } from "./proxmox-types"; @HandleIntegrationErrors([new ProxmoxApiErrorHandler()]) -export class ProxmoxIntegration extends Integration { +export class ProxmoxIntegration extends Integration implements IClusterHealthMonitoringIntegration { protected async testingAsync(input: IntegrationTestingInput): Promise { const proxmox = this.getPromoxApi(input.fetchAsync); await proxmox.nodes.$get(); diff --git a/packages/integrations/src/types.ts b/packages/integrations/src/types.ts index 2262164c4..b8dc464d4 100644 --- a/packages/integrations/src/types.ts +++ b/packages/integrations/src/types.ts @@ -1,9 +1,9 @@ -export * from "./calendar-types"; +export * from "./interfaces/calendar/calendar-types"; export * from "./interfaces/dns-hole-summary/dns-hole-summary-types"; export * from "./interfaces/network-controller-summary/network-controller-summary-types"; -export * from "./interfaces/health-monitoring/healt-monitoring"; -export * from "./interfaces/indexer-manager/indexer"; -export * from "./interfaces/media-requests/media-request"; +export * from "./interfaces/health-monitoring/health-monitoring-types"; +export * from "./interfaces/indexer-manager/indexer-manager-types"; +export * from "./interfaces/media-requests/media-request-types"; export * from "./base/searchable-integration"; export * from "./homeassistant/homeassistant-types"; export * from "./proxmox/proxmox-types"; diff --git a/packages/request-handler/src/health-monitoring.ts b/packages/request-handler/src/health-monitoring.ts index 48d95cd20..3a8959286 100644 --- a/packages/request-handler/src/health-monitoring.ts +++ b/packages/request-handler/src/health-monitoring.ts @@ -2,12 +2,12 @@ import dayjs from "dayjs"; import type { IntegrationKindByCategory } from "@homarr/definitions"; import { createIntegrationAsync } from "@homarr/integrations"; -import type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types"; +import type { ProxmoxClusterInfo, SystemHealthMonitoring } from "@homarr/integrations/types"; import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler"; export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< - HealthMonitoring, + SystemHealthMonitoring, Exclude, "proxmox">, Record >({ @@ -21,7 +21,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler< export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler< ProxmoxClusterInfo, - "proxmox", + "proxmox" | "mock", Record >({ async requestAsync(integration, _input) { diff --git a/packages/widgets/src/health-monitoring/component.tsx b/packages/widgets/src/health-monitoring/component.tsx index d1ee3dd28..baa958fe9 100644 --- a/packages/widgets/src/health-monitoring/component.tsx +++ b/packages/widgets/src/health-monitoring/component.tsx @@ -5,6 +5,7 @@ import dayjs from "dayjs"; import duration from "dayjs/plugin/duration"; import { clientApi } from "@homarr/api/client"; +import type { IntegrationKind } from "@homarr/definitions"; import { useI18n } from "@homarr/translation/client"; import type { WidgetComponentProps } from "../definition"; @@ -13,21 +14,25 @@ import { SystemHealthMonitoring } from "./system-health"; dayjs.extend(duration); +const isClusterIntegration = (integration: { kind: IntegrationKind }) => + integration.kind === "proxmox" || integration.kind === "mock"; + export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) { const [integrations] = clientApi.integration.byIds.useSuspenseQuery(props.integrationIds); const t = useI18n(); - const proxmoxIntegrationId = integrations.find((integration) => integration.kind === "proxmox")?.id; + const clusterIntegrationId = integrations.find(isClusterIntegration)?.id; - if (!proxmoxIntegrationId) { + if (!clusterIntegrationId) { return ; } const otherIntegrationIds = integrations + // We want to have the mock integration also in the system tab, so we use it for both .filter((integration) => integration.kind !== "proxmox") .map((integration) => integration.id); if (otherIntegrationIds.length === 0) { - return ; + return ; } return ( @@ -45,7 +50,7 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal - + diff --git a/packages/widgets/src/media-server/index.ts b/packages/widgets/src/media-server/index.ts index bdd6cc91c..b85d09857 100644 --- a/packages/widgets/src/media-server/index.ts +++ b/packages/widgets/src/media-server/index.ts @@ -1,5 +1,7 @@ import { IconVideo } from "@tabler/icons-react"; +import { getIntegrationKindsByCategory } from "@homarr/definitions"; + import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; @@ -10,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }), })); }, - supportedIntegrations: ["jellyfin", "plex", "emby"], + supportedIntegrations: getIntegrationKindsByCategory("mediaService"), }).withDynamicImport(() => import("./component")); diff --git a/packages/widgets/src/media-transcoding/index.ts b/packages/widgets/src/media-transcoding/index.ts index 0c6239eea..0103f480a 100644 --- a/packages/widgets/src/media-transcoding/index.ts +++ b/packages/widgets/src/media-transcoding/index.ts @@ -2,6 +2,7 @@ import { IconTransform } from "@tabler/icons-react"; import { z } from "zod"; import { capitalize } from "@homarr/common"; +import { getIntegrationKindsByCategory } from "@homarr/definitions"; import { createWidgetDefinition } from "../definition"; import { optionsBuilder } from "../options"; @@ -19,5 +20,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaTran queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }), })); }, - supportedIntegrations: ["tdarr"], + supportedIntegrations: getIntegrationKindsByCategory("mediaTranscoding"), }).withDynamicImport(() => import("./component")); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 304ba0ffa..db40deecf 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -124,6 +124,9 @@ importers: '@homarr/docker': specifier: workspace:^0.1.0 version: link:../../packages/docker + '@homarr/env': + specifier: workspace:^0.1.0 + version: link:../../packages/env '@homarr/form': specifier: workspace:^0.1.0 version: link:../../packages/form