feat(integrations): add mock integration (#3505)
This commit is contained in:
@@ -38,3 +38,6 @@ TURBO_TELEMETRY_DISABLED=1
|
|||||||
|
|
||||||
# Enable kubernetes tool
|
# Enable kubernetes tool
|
||||||
# ENABLE_KUBERNETES=true
|
# ENABLE_KUBERNETES=true
|
||||||
|
|
||||||
|
# Enable mock integration
|
||||||
|
UNSAFE_ENABLE_MOCK_INTEGRATION=true
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
"@homarr/db": "workspace:^0.1.0",
|
"@homarr/db": "workspace:^0.1.0",
|
||||||
"@homarr/definitions": "workspace:^0.1.0",
|
"@homarr/definitions": "workspace:^0.1.0",
|
||||||
"@homarr/docker": "workspace:^0.1.0",
|
"@homarr/docker": "workspace:^0.1.0",
|
||||||
|
"@homarr/env": "workspace:^0.1.0",
|
||||||
"@homarr/form": "workspace:^0.1.0",
|
"@homarr/form": "workspace:^0.1.0",
|
||||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||||
"@homarr/gridstack": "^1.12.0",
|
"@homarr/gridstack": "^1.12.0",
|
||||||
|
|||||||
BIN
apps/nextjs/public/images/mock/avatar.jpg
Normal file
BIN
apps/nextjs/public/images/mock/avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 595 KiB |
@@ -10,15 +10,20 @@ import { getIntegrationName, integrationKinds } from "@homarr/definitions";
|
|||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
import { IntegrationAvatar } from "@homarr/ui";
|
import { IntegrationAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
export const IntegrationCreateDropdownContent = () => {
|
interface IntegrationCreateDropdownContentProps {
|
||||||
|
enableMockIntegration: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const IntegrationCreateDropdownContent = ({ enableMockIntegration }: IntegrationCreateDropdownContentProps) => {
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
|
|
||||||
const filteredKinds = useMemo(() => {
|
const filteredKinds = useMemo(() => {
|
||||||
return integrationKinds.filter((kind) =>
|
return integrationKinds
|
||||||
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
|
.filter((kind) => enableMockIntegration || kind !== "mock")
|
||||||
);
|
.filter((kind) => getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()))
|
||||||
}, [search]);
|
.sort((kindA, kindB) => getIntegrationName(kindA).localeCompare(getIntegrationName(kindB)));
|
||||||
|
}, [search, enableMockIntegration]);
|
||||||
|
|
||||||
const handleSearch = React.useCallback(
|
const handleSearch = React.useCallback(
|
||||||
(event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value),
|
(event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value),
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui";
|
|||||||
import { ManageContainer } from "~/components/manage/manage-container";
|
import { ManageContainer } from "~/components/manage/manage-container";
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
import { NoResults } from "~/components/no-results";
|
import { NoResults } from "~/components/no-results";
|
||||||
|
import { env } from "~/env";
|
||||||
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
||||||
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
||||||
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
|
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
|
||||||
@@ -114,7 +115,7 @@ const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
|||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
<MenuDropdown>
|
<MenuDropdown>
|
||||||
<IntegrationCreateDropdownContent />
|
<IntegrationCreateDropdownContent enableMockIntegration={env.UNSAFE_ENABLE_MOCK_INTEGRATION} />
|
||||||
</MenuDropdown>
|
</MenuDropdown>
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
9
apps/nextjs/src/env.ts
Normal file
9
apps/nextjs/src/env.ts
Normal file
@@ -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,
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { observable } from "@trpc/server/observable";
|
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 type { ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||||
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
|||||||
|
|
||||||
export const healthMonitoringRouter = createTRPCRouter({
|
export const healthMonitoringRouter = createTRPCRouter({
|
||||||
getSystemHealthStatus: publicProcedure
|
getSystemHealthStatus: publicProcedure
|
||||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
return await Promise.all(
|
return await Promise.all(
|
||||||
ctx.integrations.map(async (integration) => {
|
ctx.integrations.map(async (integration) => {
|
||||||
@@ -26,9 +26,9 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
subscribeSystemHealthStatus: publicProcedure
|
subscribeSystemHealthStatus: publicProcedure
|
||||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
for (const integration of ctx.integrations) {
|
for (const integration of ctx.integrations) {
|
||||||
const innerHandler = systemInfoRequestHandler.handler(integration, {});
|
const innerHandler = systemInfoRequestHandler.handler(integration, {});
|
||||||
@@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
getClusterHealthStatus: publicProcedure
|
getClusterHealthStatus: publicProcedure
|
||||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock"))
|
||||||
.query(async ({ ctx }) => {
|
.query(async ({ ctx }) => {
|
||||||
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
||||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||||
return data;
|
return data;
|
||||||
}),
|
}),
|
||||||
subscribeClusterHealthStatus: publicProcedure
|
subscribeClusterHealthStatus: publicProcedure
|
||||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock"))
|
||||||
.subscription(({ ctx }) => {
|
.subscription(({ ctx }) => {
|
||||||
return observable<ProxmoxClusterInfo>((emit) => {
|
return observable<ProxmoxClusterInfo>((emit) => {
|
||||||
const unsubscribes: (() => void)[] = [];
|
const unsubscribes: (() => void)[] = [];
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC
|
|||||||
createRequestIntegrationJobHandler(
|
createRequestIntegrationJobHandler(
|
||||||
(integration, itemOptions: Record<string, never>) => {
|
(integration, itemOptions: Record<string, never>) => {
|
||||||
const { kind } = integration;
|
const { kind } = integration;
|
||||||
if (kind !== "proxmox") {
|
|
||||||
|
if (kind !== "proxmox" && kind !== "mock") {
|
||||||
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
return systemInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
||||||
}
|
}
|
||||||
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
return clusterInfoRequestHandler.handler({ ...integration, kind }, itemOptions);
|
||||||
|
|||||||
@@ -176,6 +176,25 @@ export const integrationDefs = {
|
|||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/ntfy.svg",
|
||||||
category: ["notifications"],
|
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<string, integrationDefinition>;
|
} as const satisfies Record<string, integrationDefinition>;
|
||||||
|
|
||||||
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
export const integrationKinds = objectKeys(integrationDefs) as AtLeastOneOf<IntegrationKind>;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { RadarrIntegration } from "../media-organizer/radarr/radarr-integration"
|
|||||||
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
import { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
||||||
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
||||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||||
|
import { MockIntegration } from "../mock/mock-integration";
|
||||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||||
@@ -94,6 +95,7 @@ export const integrationCreators = {
|
|||||||
nextcloud: NextcloudIntegration,
|
nextcloud: NextcloudIntegration,
|
||||||
unifiController: UnifiControllerIntegration,
|
unifiController: UnifiControllerIntegration,
|
||||||
ntfy: NTFYIntegration,
|
ntfy: NTFYIntegration,
|
||||||
|
mock: MockIntegration,
|
||||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||||
|
|
||||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const response = await input.fetchAsync(this.url("/info"));
|
const response = await input.fetchAsync(this.url("/info"));
|
||||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||||
@@ -26,7 +27,7 @@ export class DashDotIntegration extends Integration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
|
||||||
const info = await this.getInfoAsync();
|
const info = await this.getInfoAsync();
|
||||||
const cpuLoad = await this.getCurrentCpuLoadAsync();
|
const cpuLoad = await this.getCurrentCpuLoadAsync();
|
||||||
const memoryLoad = await this.getCurrentMemoryLoadAsync();
|
const memoryLoad = await this.getCurrentMemoryLoadAsync();
|
||||||
|
|||||||
@@ -4,14 +4,15 @@ import type { fetch as undiciFetch } from "undici";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
|
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<DownloadClientJobsAndStatus> {
|
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||||
const client = this.getClient();
|
const client = this.getClient();
|
||||||
const keys: (keyof Aria2Download)[] = [
|
const keys: (keyof Aria2Download)[] = [
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
|||||||
|
|
||||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||||
|
|
||||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||||
export class DelugeIntegration extends DownloadClientIntegration {
|
export class DelugeIntegration extends Integration implements IDownloadClientIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const client = await this.getClientAsync(input.dispatcher);
|
const client = await this.getClientAsync(input.dispatcher);
|
||||||
const isSuccess = await client.login();
|
const isSuccess = await client.login();
|
||||||
|
|||||||
@@ -4,15 +4,16 @@ import type { fetch as undiciFetch } from "undici";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||||
import type { NzbGetClient } from "./nzbget-types";
|
import type { NzbGetClient } from "./nzbget-types";
|
||||||
|
|
||||||
export class NzbGetIntegration extends DownloadClientIntegration {
|
export class NzbGetIntegration extends Integration implements IDownloadClientIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
|
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
|||||||
|
|
||||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||||
|
|
||||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||||
export class QBitTorrentIntegration extends DownloadClientIntegration {
|
export class QBitTorrentIntegration extends Integration implements IDownloadClientIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const client = await this.getClientAsync(input.dispatcher);
|
const client = await this.getClientAsync(input.dispatcher);
|
||||||
const isSuccess = await client.login();
|
const isSuccess = await client.login();
|
||||||
|
|||||||
@@ -5,17 +5,18 @@ import type { fetch as undiciFetch } from "undici";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||||
import { historySchema, queueSchema } from "./sabnzbd-schema";
|
import { historySchema, queueSchema } from "./sabnzbd-schema";
|
||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
export class SabnzbdIntegration extends DownloadClientIntegration {
|
export class SabnzbdIntegration extends Integration implements IDownloadClientIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
//This is the one call that uses the least amount of data while requiring the api key
|
//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" });
|
await this.sabNzbApiCallWithCustomFetchAsync(input.fetchAsync, "translate", { value: "ping" });
|
||||||
|
|||||||
@@ -6,15 +6,16 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
|||||||
|
|
||||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
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 { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||||
|
|
||||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||||
export class TransmissionIntegration extends DownloadClientIntegration {
|
export class TransmissionIntegration extends Integration implements IDownloadClientIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const client = await this.getClientAsync(input.dispatcher);
|
const client = await this.getClientAsync(input.dispatcher);
|
||||||
await client.getSession();
|
await client.getSession();
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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";
|
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
||||||
|
|
||||||
const sessionSchema = z.object({
|
const sessionSchema = z.object({
|
||||||
@@ -30,7 +31,7 @@ const sessionSchema = z.object({
|
|||||||
UserName: z.string().nullish(),
|
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 apiKeyHeader = "X-Emby-Token";
|
||||||
private static readonly deviceId = "homarr-emby-integration";
|
private static readonly deviceId = "homarr-emby-integration";
|
||||||
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
|
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
|
||||||
|
|||||||
@@ -5,9 +5,10 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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";
|
import { entityStateSchema } from "./homeassistant-types";
|
||||||
|
|
||||||
export class HomeAssistantIntegration extends Integration {
|
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration {
|
||||||
public async getEntityStateAsync(entityId: string) {
|
public async getEntityStateAsync(entityId: string) {
|
||||||
try {
|
try {
|
||||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
const response = await this.getAsync(`/api/states/${entityId}`);
|
||||||
@@ -15,6 +16,7 @@ export class HomeAssistantIntegration extends Integration {
|
|||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
logger.warn(`Response did not indicate success`);
|
logger.warn(`Response did not indicate success`);
|
||||||
return {
|
return {
|
||||||
|
success: false as const,
|
||||||
error: "Response did not indicate success",
|
error: "Response did not indicate success",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorren
|
|||||||
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
||||||
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
||||||
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
||||||
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
|
|
||||||
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
||||||
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
||||||
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-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 { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||||
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
|
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
|
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
|
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||||
export type { StreamSession } from "./interfaces/media-server/session";
|
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
||||||
export type { TdarrQueue } from "./interfaces/media-transcoding/queue";
|
export type {
|
||||||
export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics";
|
TdarrQueue,
|
||||||
export type { TdarrWorker } from "./interfaces/media-transcoding/workers";
|
TdarrPieSegment,
|
||||||
export type { Notification } from "./interfaces/notifications/notification";
|
TdarrStatistics,
|
||||||
|
TdarrWorker,
|
||||||
|
} from "./interfaces/media-transcoding/media-transcoding-types";
|
||||||
|
export type { Notification } from "./interfaces/notifications/notification-types";
|
||||||
|
|
||||||
// Schemas
|
// Schemas
|
||||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { CalendarEvent } from "./calendar-types";
|
||||||
|
|
||||||
|
export interface ICalendarIntegration {
|
||||||
|
getCalendarEventsAsync(start: Date, end: Date, includeUnmonitored: boolean): Promise<CalendarEvent[]>;
|
||||||
|
}
|
||||||
@@ -1,18 +1,17 @@
|
|||||||
import { Integration } from "../../base/integration";
|
|
||||||
import type { DownloadClientJobsAndStatus } from "./download-client-data";
|
import type { DownloadClientJobsAndStatus } from "./download-client-data";
|
||||||
import type { DownloadClientItem } from "./download-client-items";
|
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 */
|
/** Get download client's status and list of all of it's items */
|
||||||
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
||||||
/** Pauses the client or all of it's items */
|
/** Pauses the client or all of it's items */
|
||||||
public abstract pauseQueueAsync(): Promise<void>;
|
pauseQueueAsync(): Promise<void>;
|
||||||
/** Pause a single item using it's ID */
|
/** Pause a single item using it's ID */
|
||||||
public abstract pauseItemAsync(item: DownloadClientItem): Promise<void>;
|
pauseItemAsync(item: DownloadClientItem): Promise<void>;
|
||||||
/** Resumes the client or all of it's items */
|
/** Resumes the client or all of it's items */
|
||||||
public abstract resumeQueueAsync(): Promise<void>;
|
resumeQueueAsync(): Promise<void>;
|
||||||
/** Resume a single item using it's ID */
|
/** Resume a single item using it's ID */
|
||||||
public abstract resumeItemAsync(item: DownloadClientItem): Promise<void>;
|
resumeItemAsync(item: DownloadClientItem): Promise<void>;
|
||||||
/** Delete an entry on the client or a file from disk */
|
/** Delete an entry on the client or a file from disk */
|
||||||
public abstract deleteItemAsync(item: DownloadClientItem, fromDisk: boolean): Promise<void>;
|
deleteItemAsync(item: DownloadClientItem, fromDisk: boolean): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { ClusterHealthMonitoring, SystemHealthMonitoring } from "./health-monitoring-types";
|
||||||
|
|
||||||
|
export interface ISystemHealthMonitoringIntegration {
|
||||||
|
getSystemInfoAsync(): Promise<SystemHealthMonitoring>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IClusterHealthMonitoringIntegration {
|
||||||
|
getClusterInfoAsync(): Promise<ClusterHealthMonitoring>;
|
||||||
|
}
|
||||||
@@ -1,4 +1,6 @@
|
|||||||
export interface HealthMonitoring {
|
import type { LxcResource, NodeResource, QemuResource, StorageResource } from "../../types";
|
||||||
|
|
||||||
|
export interface SystemHealthMonitoring {
|
||||||
version: string;
|
version: string;
|
||||||
cpuModelName: string;
|
cpuModelName: string;
|
||||||
cpuUtilization: number;
|
cpuUtilization: number;
|
||||||
@@ -25,3 +27,11 @@ export interface HealthMonitoring {
|
|||||||
overallStatus: string;
|
overallStatus: string;
|
||||||
}[];
|
}[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: in the future decouple this from the Proxmox integration
|
||||||
|
export interface ClusterHealthMonitoring {
|
||||||
|
nodes: NodeResource[];
|
||||||
|
lxcs: LxcResource[];
|
||||||
|
vms: QemuResource[];
|
||||||
|
storages: StorageResource[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
import type { Indexer } from "./indexer-manager-types";
|
||||||
|
|
||||||
|
export interface IIndexerManagerIntegration {
|
||||||
|
getIndexersAsync(): Promise<Indexer[]>;
|
||||||
|
testAllAsync(): Promise<void>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import type { MediaInformation, MediaRequest, RequestStats, RequestUser } from "./media-request-types";
|
||||||
|
|
||||||
|
export interface IMediaRequestIntegration {
|
||||||
|
getSeriesInformationAsync(mediaType: "movie" | "tv", id: number): Promise<MediaInformation>;
|
||||||
|
requestMediaAsync(mediaType: "movie" | "tv", id: number, seasons?: number[]): Promise<void>;
|
||||||
|
getRequestsAsync(): Promise<MediaRequest[]>;
|
||||||
|
getStatsAsync(): Promise<RequestStats>;
|
||||||
|
getUsersAsync(): Promise<RequestUser[]>;
|
||||||
|
approveRequestAsync(requestId: number): Promise<void>;
|
||||||
|
declineRequestAsync(requestId: number): Promise<void>;
|
||||||
|
}
|
||||||
@@ -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 {
|
export interface MediaRequest {
|
||||||
id: number;
|
id: number;
|
||||||
name: string;
|
name: string;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { CurrentSessionsInput, StreamSession } from "./media-server-types";
|
||||||
|
|
||||||
|
export interface IMediaServerIntegration {
|
||||||
|
getCurrentSessionsAsync(options: CurrentSessionsInput): Promise<StreamSession[]>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "./media-transcoding-types";
|
||||||
|
|
||||||
|
export interface IMediaTranscodingIntegration {
|
||||||
|
getStatisticsAsync(): Promise<TdarrStatistics>;
|
||||||
|
getWorkersAsync(): Promise<TdarrWorker[]>;
|
||||||
|
getQueueAsync(firstItemIndex: number, pageSize: number): Promise<TdarrQueue>;
|
||||||
|
}
|
||||||
@@ -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 {
|
export interface TdarrPieSegment {
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: number;
|
||||||
@@ -21,3 +38,17 @@ export interface TdarrStatistics {
|
|||||||
audioCodecs: TdarrPieSegment[];
|
audioCodecs: TdarrPieSegment[];
|
||||||
audioContainers: 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;
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Integration } from "../../base/integration";
|
import type { Notification } from "./notification-types";
|
||||||
import type { Notification } from "./notification";
|
|
||||||
|
|
||||||
export abstract class NotificationsIntegration extends Integration {
|
export interface INotificationsIntegration {
|
||||||
public abstract getNotificationsAsync(): Promise<Notification[]>;
|
getNotificationsAsync(): Promise<Notification[]>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import type { EntityStateResult } from "./smart-home-types";
|
||||||
|
|
||||||
|
export interface ISmartHomeIntegration {
|
||||||
|
getEntityStateAsync(entityId: string): Promise<EntityStateResult>;
|
||||||
|
triggerAutomationAsync(entityId: string): Promise<boolean>;
|
||||||
|
triggerToggleAsync(entityId: string): Promise<boolean>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
interface EntityState {
|
||||||
|
attributes: Record<string, string | number | boolean | null | (string | number)[]>;
|
||||||
|
entity_id: string;
|
||||||
|
last_changed: Date;
|
||||||
|
last_updated: Date;
|
||||||
|
state: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type EntityStateResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
data: EntityState;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
success: false;
|
||||||
|
error: unknown;
|
||||||
|
};
|
||||||
@@ -11,10 +11,11 @@ import { integrationAxiosHttpErrorHandler } from "../base/errors/http";
|
|||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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])
|
@HandleIntegrationErrors([integrationAxiosHttpErrorHandler])
|
||||||
export class JellyfinIntegration extends Integration {
|
export class JellyfinIntegration extends Integration implements IMediaServerIntegration {
|
||||||
private readonly jellyfin: Jellyfin = new Jellyfin({
|
private readonly jellyfin: Jellyfin = new Jellyfin({
|
||||||
clientInfo: {
|
clientInfo: {
|
||||||
name: "Homarr",
|
name: "Homarr",
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
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 { MediaOrganizerIntegration } from "../media-organizer-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<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const response = await input.fetchAsync(this.url("/api"), {
|
const response = await input.fetchAsync(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
@@ -103,7 +105,8 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
|||||||
const flatImages = [...event.images];
|
const flatImages = [...event.images];
|
||||||
|
|
||||||
const sortedImages = flatImages.sort(
|
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(",")}]`);
|
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||||
return sortedImages[0];
|
return sortedImages[0];
|
||||||
|
|||||||
@@ -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
|
|
||||||
];
|
|
||||||
}
|
|
||||||
17
packages/integrations/src/media-organizer/media-organizer.ts
Normal file
17
packages/integrations/src/media-organizer/media-organizer.ts
Normal file
@@ -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
|
||||||
|
];
|
||||||
@@ -4,14 +4,16 @@ import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
|||||||
import type { AtLeastOneOf } from "@homarr/common/types";
|
import type { AtLeastOneOf } from "@homarr/common/types";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
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 { radarrReleaseTypes } from "../../calendar-types";
|
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||||
import { MediaOrganizerIntegration } from "../media-organizer-integration";
|
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.
|
* Gets the events in the Radarr calendar between two dates.
|
||||||
* @param start The start date
|
* @param start The start date
|
||||||
@@ -82,7 +84,8 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
const flatImages = [...event.images];
|
const flatImages = [...event.images];
|
||||||
|
|
||||||
const sortedImages = flatImages.sort(
|
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(",")}]`);
|
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||||
return sortedImages[0];
|
return sortedImages[0];
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
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 { MediaOrganizerIntegration } from "../media-organizer-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<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const response = await input.fetchAsync(this.url("/api"), {
|
const response = await input.fetchAsync(this.url("/api"), {
|
||||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||||
@@ -81,7 +83,8 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
|||||||
const flatImages = [...event.images];
|
const flatImages = [...event.images];
|
||||||
|
|
||||||
const sortedImages = flatImages.sort(
|
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(",")}]`);
|
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||||
return sortedImages[0];
|
return sortedImages[0];
|
||||||
|
|||||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
|
|
||||||
|
import { Integration } from "../../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../../base/integration";
|
import type { IntegrationTestingInput } from "../../base/integration";
|
||||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
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 { MediaOrganizerIntegration } from "../media-organizer-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.
|
* Gets the events in the Sonarr calendar between two dates.
|
||||||
* @param start The start date
|
* @param start The start date
|
||||||
@@ -81,7 +83,8 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
|||||||
const flatImages = [...event.images, ...event.series.images];
|
const flatImages = [...event.images, ...event.series.images];
|
||||||
|
|
||||||
const sortedImages = flatImages.sort(
|
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(",")}]`);
|
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||||
return sortedImages[0];
|
return sortedImages[0];
|
||||||
|
|||||||
@@ -4,12 +4,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { TdarrQueue } from "../interfaces/media-transcoding/queue";
|
import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration";
|
||||||
import type { TdarrStatistics } from "../interfaces/media-transcoding/statistics";
|
import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "../interfaces/media-transcoding/media-transcoding-types";
|
||||||
import type { TdarrWorker } from "../interfaces/media-transcoding/workers";
|
|
||||||
import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } from "./tdarr-validation-schemas";
|
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<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const response = await input.fetchAsync(this.url("/api/v2/is-server-alive"), {
|
const response = await input.fetchAsync(this.url("/api/v2/is-server-alive"), {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
74
packages/integrations/src/mock/data/calendar.ts
Normal file
74
packages/integrations/src/mock/data/calendar.ts
Normal file
@@ -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<CalendarEvent[]> {
|
||||||
|
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()));
|
||||||
|
}
|
||||||
100
packages/integrations/src/mock/data/cluster-health-monitoring.ts
Normal file
100
packages/integrations/src/mock/data/cluster-health-monitoring.ts
Normal file
@@ -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<ClusterHealthMonitoring> {
|
||||||
|
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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
26
packages/integrations/src/mock/data/dns-hole.ts
Normal file
26
packages/integrations/src/mock/data/dns-hole.ts
Normal file
@@ -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<DnsHoleSummary> {
|
||||||
|
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<void> {
|
||||||
|
DnsHoleMockService.isEnabled = true;
|
||||||
|
return await Promise.resolve();
|
||||||
|
}
|
||||||
|
public async disableAsync(_duration?: number): Promise<void> {
|
||||||
|
DnsHoleMockService.isEnabled = false;
|
||||||
|
return await Promise.resolve();
|
||||||
|
}
|
||||||
|
}
|
||||||
58
packages/integrations/src/mock/data/download.ts
Normal file
58
packages/integrations/src/mock/data/download.ts
Normal file
@@ -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<DownloadClientJobsAndStatus> {
|
||||||
|
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<void> {
|
||||||
|
return await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async pauseItemAsync(_item: DownloadClientItem): Promise<void> {
|
||||||
|
return await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resumeQueueAsync(): Promise<void> {
|
||||||
|
return Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async resumeItemAsync(_item: DownloadClientItem): Promise<void> {
|
||||||
|
return await Promise.resolve();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async deleteItemAsync(_item: DownloadClientItem, _fromDisk: boolean): Promise<void> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
23
packages/integrations/src/mock/data/indexer-manager.ts
Normal file
23
packages/integrations/src/mock/data/indexer-manager.ts
Normal file
@@ -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<Indexer[]> {
|
||||||
|
return await Promise.resolve(
|
||||||
|
Array.from({ length: 10 }, (_, index) => IndexerManagerMockService.createIndexer(index + 1)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public async testAllAsync(): Promise<void> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/integrations/src/mock/data/media-request.ts
Normal file
97
packages/integrations/src/mock/data/media-request.ts
Normal file
@@ -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<MediaInformation> {
|
||||||
|
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<void> {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
public async getRequestsAsync(): Promise<MediaRequest[]> {
|
||||||
|
const result = await Promise.resolve(
|
||||||
|
Array.from({ length: 10 }, (_, index) => MediaRequestMockService.createRequest(index)),
|
||||||
|
);
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
public async getStatsAsync(): Promise<RequestStats> {
|
||||||
|
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<RequestUser[]> {
|
||||||
|
return await Promise.resolve(Array.from({ length: 5 }, (_, index) => MediaRequestMockService.createUser(index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async approveRequestAsync(_requestId: number): Promise<void> {
|
||||||
|
await Promise.resolve();
|
||||||
|
}
|
||||||
|
public async declineRequestAsync(_requestId: number): Promise<void> {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
packages/integrations/src/mock/data/media-server.ts
Normal file
35
packages/integrations/src/mock/data/media-server.ts
Normal file
@@ -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<StreamSession[]> {
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
68
packages/integrations/src/mock/data/media-transcoding.ts
Normal file
68
packages/integrations/src/mock/data/media-transcoding.ts
Normal file
@@ -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<TdarrStatistics> {
|
||||||
|
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<TdarrWorker[]> {
|
||||||
|
return await Promise.resolve(
|
||||||
|
Array.from({ length: 5 }, (_, index) => MediaTranscodingMockService.createWorker(index)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
public async getQueueAsync(firstItemIndex: number, pageSize: number): Promise<TdarrQueue> {
|
||||||
|
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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<NetworkControllerSummary> {
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
19
packages/integrations/src/mock/data/notifications.ts
Normal file
19
packages/integrations/src/mock/data/notifications.ts
Normal file
@@ -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<Notification[]> {
|
||||||
|
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}.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
27
packages/integrations/src/mock/data/smart-home.ts
Normal file
27
packages/integrations/src/mock/data/smart-home.ts
Normal file
@@ -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<EntityStateResult> {
|
||||||
|
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<boolean> {
|
||||||
|
return await Promise.resolve(true);
|
||||||
|
}
|
||||||
|
public async triggerToggleAsync(_entityId: string): Promise<boolean> {
|
||||||
|
return await Promise.resolve(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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<SystemHealthMonitoring> {
|
||||||
|
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",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
119
packages/integrations/src/mock/mock-integration.ts
Normal file
119
packages/integrations/src/mock/mock-integration.ts
Normal file
@@ -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<TestingResult> {
|
||||||
|
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);
|
||||||
|
}
|
||||||
@@ -10,10 +10,11 @@ import { integrationTsdavHttpErrorHandler } from "../base/errors/http";
|
|||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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])
|
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
|
||||||
export class NextcloudIntegration extends Integration {
|
export class NextcloudIntegration extends Integration implements ICalendarIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const client = await this.createCalendarClientAsync(input.dispatcher);
|
const client = await this.createCalendarClientAsync(input.dispatcher);
|
||||||
await client.login();
|
await client.login();
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||||
import { ResponseError } from "@homarr/common/server";
|
import { ResponseError } from "@homarr/common/server";
|
||||||
|
|
||||||
|
import { Integration } from "../base/integration";
|
||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { Notification } from "../interfaces/notifications/notification";
|
import type { Notification } from "../interfaces/notifications/notification-types";
|
||||||
import { NotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
||||||
import { ntfyNotificationSchema } from "./ntfy-schema";
|
import { ntfyNotificationSchema } from "./ntfy-schema";
|
||||||
|
|
||||||
export class NTFYIntegration extends NotificationsIntegration {
|
export class NTFYIntegration extends Integration implements INotificationsIntegration {
|
||||||
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
|
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import { Integration } from "../base/integration";
|
|||||||
import type { SessionStore } from "../base/session-store";
|
import type { SessionStore } from "../base/session-store";
|
||||||
import { createSessionStore } from "../base/session-store";
|
import { createSessionStore } from "../base/session-store";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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";
|
import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types";
|
||||||
|
|
||||||
const localLogger = logger.child({ module: "OpenMediaVaultIntegration" });
|
const localLogger = logger.child({ module: "OpenMediaVaultIntegration" });
|
||||||
@@ -18,7 +19,7 @@ type SessionStoreValue =
|
|||||||
| { type: "header"; sessionId: string }
|
| { type: "header"; sessionId: string }
|
||||||
| { type: "cookie"; loginToken: string; sessionId: string };
|
| { type: "cookie"; loginToken: string; sessionId: string };
|
||||||
|
|
||||||
export class OpenMediaVaultIntegration extends Integration {
|
export class OpenMediaVaultIntegration extends Integration implements ISystemHealthMonitoringIntegration {
|
||||||
private readonly sessionStore: SessionStore<SessionStoreValue>;
|
private readonly sessionStore: SessionStore<SessionStoreValue>;
|
||||||
|
|
||||||
constructor(integration: IntegrationInput) {
|
constructor(integration: IntegrationInput) {
|
||||||
@@ -26,7 +27,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
|||||||
this.sessionStore = createSessionStore(integration);
|
this.sessionStore = createSessionStore(integration);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
|
||||||
const systemResponses = await this.makeAuthenticatedRpcCallAsync("system", "getInformation");
|
const systemResponses = await this.makeAuthenticatedRpcCallAsync("system", "getInformation");
|
||||||
const fileSystemResponse = await this.makeAuthenticatedRpcCallAsync(
|
const fileSystemResponse = await this.makeAuthenticatedRpcCallAsync(
|
||||||
"filesystemmgmt",
|
"filesystemmgmt",
|
||||||
|
|||||||
@@ -8,8 +8,9 @@ import { Integration } from "../base/integration";
|
|||||||
import type { ISearchableIntegration } from "../base/searchable-integration";
|
import type { ISearchableIntegration } from "../base/searchable-integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||||
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request";
|
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
||||||
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request";
|
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types";
|
||||||
|
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types";
|
||||||
|
|
||||||
interface OverseerrSearchResult {
|
interface OverseerrSearchResult {
|
||||||
id: number;
|
id: number;
|
||||||
@@ -23,7 +24,10 @@ interface OverseerrSearchResult {
|
|||||||
/**
|
/**
|
||||||
* Overseerr Integration. See https://api-docs.overseerr.dev
|
* Overseerr Integration. See https://api-docs.overseerr.dev
|
||||||
*/
|
*/
|
||||||
export class OverseerrIntegration extends Integration implements ISearchableIntegration<OverseerrSearchResult> {
|
export class OverseerrIntegration
|
||||||
|
extends Integration
|
||||||
|
implements IMediaRequestIntegration, ISearchableIntegration<OverseerrSearchResult>
|
||||||
|
{
|
||||||
public async searchAsync(query: string) {
|
public async searchAsync(query: string) {
|
||||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), {
|
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -8,10 +8,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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";
|
import type { PlexResponse } from "./interface";
|
||||||
|
|
||||||
export class PlexIntegration extends Integration {
|
export class PlexIntegration extends Integration implements IMediaServerIntegration {
|
||||||
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||||
const token = super.getSecretValue("apiKey");
|
const token = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
|||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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";
|
import { indexerResponseSchema, statusResponseSchema } from "./prowlarr-types";
|
||||||
|
|
||||||
export class ProwlarrIntegration extends Integration {
|
export class ProwlarrIntegration extends Integration implements IIndexerManagerIntegration {
|
||||||
public async getIndexersAsync(): Promise<Indexer[]> {
|
public async getIndexersAsync(): Promise<Indexer[]> {
|
||||||
const apiKey = super.getSecretValue("apiKey");
|
const apiKey = super.getSecretValue("apiKey");
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { HandleIntegrationErrors } from "../base/errors/decorator";
|
|||||||
import type { IntegrationTestingInput } from "../base/integration";
|
import type { IntegrationTestingInput } from "../base/integration";
|
||||||
import { Integration } from "../base/integration";
|
import { Integration } from "../base/integration";
|
||||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
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 { ProxmoxApiErrorHandler } from "./proxmox-error-handler";
|
||||||
import type {
|
import type {
|
||||||
ComputeResourceBase,
|
ComputeResourceBase,
|
||||||
@@ -19,7 +20,7 @@ import type {
|
|||||||
} from "./proxmox-types";
|
} from "./proxmox-types";
|
||||||
|
|
||||||
@HandleIntegrationErrors([new ProxmoxApiErrorHandler()])
|
@HandleIntegrationErrors([new ProxmoxApiErrorHandler()])
|
||||||
export class ProxmoxIntegration extends Integration {
|
export class ProxmoxIntegration extends Integration implements IClusterHealthMonitoringIntegration {
|
||||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||||
const proxmox = this.getPromoxApi(input.fetchAsync);
|
const proxmox = this.getPromoxApi(input.fetchAsync);
|
||||||
await proxmox.nodes.$get();
|
await proxmox.nodes.$get();
|
||||||
|
|||||||
@@ -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/dns-hole-summary/dns-hole-summary-types";
|
||||||
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
export * from "./interfaces/network-controller-summary/network-controller-summary-types";
|
||||||
export * from "./interfaces/health-monitoring/healt-monitoring";
|
export * from "./interfaces/health-monitoring/health-monitoring-types";
|
||||||
export * from "./interfaces/indexer-manager/indexer";
|
export * from "./interfaces/indexer-manager/indexer-manager-types";
|
||||||
export * from "./interfaces/media-requests/media-request";
|
export * from "./interfaces/media-requests/media-request-types";
|
||||||
export * from "./base/searchable-integration";
|
export * from "./base/searchable-integration";
|
||||||
export * from "./homeassistant/homeassistant-types";
|
export * from "./homeassistant/homeassistant-types";
|
||||||
export * from "./proxmox/proxmox-types";
|
export * from "./proxmox/proxmox-types";
|
||||||
|
|||||||
@@ -2,12 +2,12 @@ import dayjs from "dayjs";
|
|||||||
|
|
||||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
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";
|
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||||
|
|
||||||
export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
HealthMonitoring,
|
SystemHealthMonitoring,
|
||||||
Exclude<IntegrationKindByCategory<"healthMonitoring">, "proxmox">,
|
Exclude<IntegrationKindByCategory<"healthMonitoring">, "proxmox">,
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
@@ -21,7 +21,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
|||||||
|
|
||||||
export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler<
|
export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||||
ProxmoxClusterInfo,
|
ProxmoxClusterInfo,
|
||||||
"proxmox",
|
"proxmox" | "mock",
|
||||||
Record<string, never>
|
Record<string, never>
|
||||||
>({
|
>({
|
||||||
async requestAsync(integration, _input) {
|
async requestAsync(integration, _input) {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import dayjs from "dayjs";
|
|||||||
import duration from "dayjs/plugin/duration";
|
import duration from "dayjs/plugin/duration";
|
||||||
|
|
||||||
import { clientApi } from "@homarr/api/client";
|
import { clientApi } from "@homarr/api/client";
|
||||||
|
import type { IntegrationKind } from "@homarr/definitions";
|
||||||
import { useI18n } from "@homarr/translation/client";
|
import { useI18n } from "@homarr/translation/client";
|
||||||
|
|
||||||
import type { WidgetComponentProps } from "../definition";
|
import type { WidgetComponentProps } from "../definition";
|
||||||
@@ -13,21 +14,25 @@ import { SystemHealthMonitoring } from "./system-health";
|
|||||||
|
|
||||||
dayjs.extend(duration);
|
dayjs.extend(duration);
|
||||||
|
|
||||||
|
const isClusterIntegration = (integration: { kind: IntegrationKind }) =>
|
||||||
|
integration.kind === "proxmox" || integration.kind === "mock";
|
||||||
|
|
||||||
export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) {
|
export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) {
|
||||||
const [integrations] = clientApi.integration.byIds.useSuspenseQuery(props.integrationIds);
|
const [integrations] = clientApi.integration.byIds.useSuspenseQuery(props.integrationIds);
|
||||||
const t = useI18n();
|
const t = useI18n();
|
||||||
|
|
||||||
const proxmoxIntegrationId = integrations.find((integration) => integration.kind === "proxmox")?.id;
|
const clusterIntegrationId = integrations.find(isClusterIntegration)?.id;
|
||||||
|
|
||||||
if (!proxmoxIntegrationId) {
|
if (!clusterIntegrationId) {
|
||||||
return <SystemHealthMonitoring {...props} />;
|
return <SystemHealthMonitoring {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const otherIntegrationIds = integrations
|
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")
|
.filter((integration) => integration.kind !== "proxmox")
|
||||||
.map((integration) => integration.id);
|
.map((integration) => integration.id);
|
||||||
if (otherIntegrationIds.length === 0) {
|
if (otherIntegrationIds.length === 0) {
|
||||||
return <ClusterHealthMonitoring {...props} integrationId={proxmoxIntegrationId} />;
|
return <ClusterHealthMonitoring {...props} integrationId={clusterIntegrationId} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -45,7 +50,7 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal
|
|||||||
<SystemHealthMonitoring {...props} integrationIds={otherIntegrationIds} />
|
<SystemHealthMonitoring {...props} integrationIds={otherIntegrationIds} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
<Tabs.Panel value="cluster">
|
<Tabs.Panel value="cluster">
|
||||||
<ClusterHealthMonitoring integrationId={proxmoxIntegrationId} {...props} />
|
<ClusterHealthMonitoring integrationId={clusterIntegrationId} {...props} />
|
||||||
</Tabs.Panel>
|
</Tabs.Panel>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { IconVideo } from "@tabler/icons-react";
|
import { IconVideo } from "@tabler/icons-react";
|
||||||
|
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
import { optionsBuilder } from "../options";
|
||||||
|
|
||||||
@@ -10,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ
|
|||||||
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
|
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
supportedIntegrations: getIntegrationKindsByCategory("mediaService"),
|
||||||
}).withDynamicImport(() => import("./component"));
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IconTransform } from "@tabler/icons-react";
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { capitalize } from "@homarr/common";
|
import { capitalize } from "@homarr/common";
|
||||||
|
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||||
|
|
||||||
import { createWidgetDefinition } from "../definition";
|
import { createWidgetDefinition } from "../definition";
|
||||||
import { optionsBuilder } from "../options";
|
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) }),
|
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
supportedIntegrations: ["tdarr"],
|
supportedIntegrations: getIntegrationKindsByCategory("mediaTranscoding"),
|
||||||
}).withDynamicImport(() => import("./component"));
|
}).withDynamicImport(() => import("./component"));
|
||||||
|
|||||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -124,6 +124,9 @@ importers:
|
|||||||
'@homarr/docker':
|
'@homarr/docker':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/docker
|
version: link:../../packages/docker
|
||||||
|
'@homarr/env':
|
||||||
|
specifier: workspace:^0.1.0
|
||||||
|
version: link:../../packages/env
|
||||||
'@homarr/form':
|
'@homarr/form':
|
||||||
specifier: workspace:^0.1.0
|
specifier: workspace:^0.1.0
|
||||||
version: link:../../packages/form
|
version: link:../../packages/form
|
||||||
|
|||||||
Reference in New Issue
Block a user