mirror of
https://github.com/ajnart/homarr.git
synced 2026-02-26 08:20:56 +01:00
feat(integrations): add mock integration (#3505)
This commit is contained in:
@@ -37,4 +37,7 @@ DB_URL='FULL_PATH_TO_YOUR_SQLITE_DB_FILE'
|
||||
TURBO_TELEMETRY_DISABLED=1
|
||||
|
||||
# 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/definitions": "workspace:^0.1.0",
|
||||
"@homarr/docker": "workspace:^0.1.0",
|
||||
"@homarr/env": "workspace:^0.1.0",
|
||||
"@homarr/form": "workspace:^0.1.0",
|
||||
"@homarr/forms-collection": "workspace:^0.1.0",
|
||||
"@homarr/gridstack": "^1.12.0",
|
||||
|
||||
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 { IntegrationAvatar } from "@homarr/ui";
|
||||
|
||||
export const IntegrationCreateDropdownContent = () => {
|
||||
interface IntegrationCreateDropdownContentProps {
|
||||
enableMockIntegration: boolean;
|
||||
}
|
||||
|
||||
export const IntegrationCreateDropdownContent = ({ enableMockIntegration }: IntegrationCreateDropdownContentProps) => {
|
||||
const t = useI18n();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const filteredKinds = useMemo(() => {
|
||||
return integrationKinds.filter((kind) =>
|
||||
getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()),
|
||||
);
|
||||
}, [search]);
|
||||
return integrationKinds
|
||||
.filter((kind) => enableMockIntegration || kind !== "mock")
|
||||
.filter((kind) => getIntegrationName(kind).toLowerCase().includes(search.toLowerCase().trim()))
|
||||
.sort((kindA, kindB) => getIntegrationName(kindA).localeCompare(getIntegrationName(kindB)));
|
||||
}, [search, enableMockIntegration]);
|
||||
|
||||
const handleSearch = React.useCallback(
|
||||
(event: ChangeEvent<HTMLInputElement>) => setSearch(event.target.value),
|
||||
|
||||
@@ -41,6 +41,7 @@ import { CountBadge, IntegrationAvatar } from "@homarr/ui";
|
||||
import { ManageContainer } from "~/components/manage/manage-container";
|
||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||
import { NoResults } from "~/components/no-results";
|
||||
import { env } from "~/env";
|
||||
import { ActiveTabAccordion } from "../../../../components/active-tab-accordion";
|
||||
import { DeleteIntegrationActionButton } from "./_integration-buttons";
|
||||
import { IntegrationCreateDropdownContent } from "./new/_integration-new-dropdown";
|
||||
@@ -114,7 +115,7 @@ const IntegrationSelectMenu = ({ children }: PropsWithChildren) => {
|
||||
>
|
||||
{children}
|
||||
<MenuDropdown>
|
||||
<IntegrationCreateDropdownContent />
|
||||
<IntegrationCreateDropdownContent enableMockIntegration={env.UNSAFE_ENABLE_MOCK_INTEGRATION} />
|
||||
</MenuDropdown>
|
||||
</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 type { HealthMonitoring } from "@homarr/integrations";
|
||||
import type { SystemHealthMonitoring } from "@homarr/integrations";
|
||||
import type { ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||
import { clusterInfoRequestHandler, systemInfoRequestHandler } from "@homarr/request-handler/health-monitoring";
|
||||
|
||||
@@ -9,7 +9,7 @@ import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const healthMonitoringRouter = createTRPCRouter({
|
||||
getSystemHealthStatus: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||
.query(async ({ ctx }) => {
|
||||
return await Promise.all(
|
||||
ctx.integrations.map(async (integration) => {
|
||||
@@ -26,9 +26,9 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
);
|
||||
}),
|
||||
subscribeSystemHealthStatus: publicProcedure
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot"))
|
||||
.concat(createManyIntegrationMiddleware("query", "openmediavault", "dashDot", "mock"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<{ integrationId: string; healthInfo: HealthMonitoring; timestamp: Date }>((emit) => {
|
||||
return observable<{ integrationId: string; healthInfo: SystemHealthMonitoring; timestamp: Date }>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
for (const integration of ctx.integrations) {
|
||||
const innerHandler = systemInfoRequestHandler.handler(integration, {});
|
||||
@@ -49,14 +49,14 @@ export const healthMonitoringRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
getClusterHealthStatus: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock"))
|
||||
.query(async ({ ctx }) => {
|
||||
const innerHandler = clusterInfoRequestHandler.handler(ctx.integration, {});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data;
|
||||
}),
|
||||
subscribeClusterHealthStatus: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox"))
|
||||
.concat(createOneIntegrationMiddleware("query", "proxmox", "mock"))
|
||||
.subscription(({ ctx }) => {
|
||||
return observable<ProxmoxClusterInfo>((emit) => {
|
||||
const unsubscribes: (() => void)[] = [];
|
||||
|
||||
@@ -8,7 +8,8 @@ export const healthMonitoringJob = createCronJob("healthMonitoring", EVERY_5_SEC
|
||||
createRequestIntegrationJobHandler(
|
||||
(integration, itemOptions: Record<string, never>) => {
|
||||
const { kind } = integration;
|
||||
if (kind !== "proxmox") {
|
||||
|
||||
if (kind !== "proxmox" && kind !== "mock") {
|
||||
return systemInfoRequestHandler.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",
|
||||
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>;
|
||||
|
||||
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 { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||
import { MockIntegration } from "../mock/mock-integration";
|
||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||
import { NTFYIntegration } from "../ntfy/ntfy-integration";
|
||||
import { OpenMediaVaultIntegration } from "../openmediavault/openmediavault-integration";
|
||||
@@ -94,6 +95,7 @@ export const integrationCreators = {
|
||||
nextcloud: NextcloudIntegration,
|
||||
unifiController: UnifiControllerIntegration,
|
||||
ntfy: NTFYIntegration,
|
||||
mock: MockIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
|
||||
@@ -12,9 +12,10 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { HealthMonitoring } from "../types";
|
||||
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
|
||||
import type { SystemHealthMonitoring } from "../interfaces/health-monitoring/health-monitoring-types";
|
||||
|
||||
export class DashDotIntegration extends Integration {
|
||||
export class DashDotIntegration extends Integration implements ISystemHealthMonitoringIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/info"));
|
||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||
@@ -26,7 +27,7 @@ export class DashDotIntegration extends Integration {
|
||||
};
|
||||
}
|
||||
|
||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
||||
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
|
||||
const info = await this.getInfoAsync();
|
||||
const cpuLoad = await this.getCurrentCpuLoadAsync();
|
||||
const memoryLoad = await this.getCurrentMemoryLoadAsync();
|
||||
|
||||
@@ -4,14 +4,15 @@ import type { fetch as undiciFetch } from "undici";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { Aria2Download, Aria2GetClient } from "./aria2-types";
|
||||
|
||||
export class Aria2Integration extends DownloadClientIntegration {
|
||||
export class Aria2Integration extends Integration implements IDownloadClientIntegration {
|
||||
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
|
||||
const client = this.getClient();
|
||||
const keys: (keyof Aria2Download)[] = [
|
||||
|
||||
@@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||
|
||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||
export class DelugeIntegration extends DownloadClientIntegration {
|
||||
export class DelugeIntegration extends Integration implements IDownloadClientIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const client = await this.getClientAsync(input.dispatcher);
|
||||
const isSuccess = await client.login();
|
||||
|
||||
@@ -4,15 +4,16 @@ import type { fetch as undiciFetch } from "undici";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||
import type { NzbGetClient } from "./nzbget-types";
|
||||
|
||||
export class NzbGetIntegration extends DownloadClientIntegration {
|
||||
export class NzbGetIntegration extends Integration implements IDownloadClientIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
|
||||
return {
|
||||
|
||||
@@ -6,16 +6,17 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||
|
||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||
export class QBitTorrentIntegration extends DownloadClientIntegration {
|
||||
export class QBitTorrentIntegration extends Integration implements IDownloadClientIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const client = await this.getClientAsync(input.dispatcher);
|
||||
const isSuccess = await client.login();
|
||||
|
||||
@@ -5,17 +5,18 @@ import type { fetch as undiciFetch } from "undici";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||
import { historySchema, queueSchema } from "./sabnzbd-schema";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
export class SabnzbdIntegration extends DownloadClientIntegration {
|
||||
export class SabnzbdIntegration extends Integration implements IDownloadClientIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
//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" });
|
||||
|
||||
@@ -6,15 +6,16 @@ import { createCertificateAgentAsync } from "@homarr/certificates/server";
|
||||
|
||||
import { HandleIntegrationErrors } from "../../base/errors/decorator";
|
||||
import { integrationOFetchHttpErrorHandler } from "../../base/errors/http";
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
|
||||
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { IDownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
|
||||
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
|
||||
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
|
||||
|
||||
@HandleIntegrationErrors([integrationOFetchHttpErrorHandler])
|
||||
export class TransmissionIntegration extends DownloadClientIntegration {
|
||||
export class TransmissionIntegration extends Integration implements IDownloadClientIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const client = await this.getClientAsync(input.dispatcher);
|
||||
await client.getSession();
|
||||
|
||||
@@ -7,7 +7,8 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
|
||||
import { convertJellyfinType } from "../jellyfin/jellyfin-integration";
|
||||
|
||||
const sessionSchema = z.object({
|
||||
@@ -30,7 +31,7 @@ const sessionSchema = z.object({
|
||||
UserName: z.string().nullish(),
|
||||
});
|
||||
|
||||
export class EmbyIntegration extends Integration {
|
||||
export class EmbyIntegration extends Integration implements IMediaServerIntegration {
|
||||
private static readonly apiKeyHeader = "X-Emby-Token";
|
||||
private static readonly deviceId = "homarr-emby-integration";
|
||||
private static readonly authorizationHeaderValue = `Emby Client="Dashboard", Device="Homarr", DeviceId="${EmbyIntegration.deviceId}", Version="0.0.1"`;
|
||||
|
||||
@@ -5,9 +5,10 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { ISmartHomeIntegration } from "../interfaces/smart-home/smart-home-integration";
|
||||
import { entityStateSchema } from "./homeassistant-types";
|
||||
|
||||
export class HomeAssistantIntegration extends Integration {
|
||||
export class HomeAssistantIntegration extends Integration implements ISmartHomeIntegration {
|
||||
public async getEntityStateAsync(entityId: string) {
|
||||
try {
|
||||
const response = await this.getAsync(`/api/states/${entityId}`);
|
||||
@@ -15,6 +16,7 @@ export class HomeAssistantIntegration extends Integration {
|
||||
if (!response.ok) {
|
||||
logger.warn(`Response did not indicate success`);
|
||||
return {
|
||||
success: false as const,
|
||||
error: "Response did not indicate success",
|
||||
};
|
||||
}
|
||||
|
||||
@@ -7,7 +7,6 @@ export { QBitTorrentIntegration } from "./download-client/qbittorrent/qbittorren
|
||||
export { SabnzbdIntegration } from "./download-client/sabnzbd/sabnzbd-integration";
|
||||
export { TransmissionIntegration } from "./download-client/transmission/transmission-integration";
|
||||
export { HomeAssistantIntegration } from "./homeassistant/homeassistant-integration";
|
||||
export { DownloadClientIntegration } from "./interfaces/downloads/download-client-integration";
|
||||
export { JellyfinIntegration } from "./jellyfin/jellyfin-integration";
|
||||
export { JellyseerrIntegration } from "./jellyseerr/jellyseerr-integration";
|
||||
export { LidarrIntegration } from "./media-organizer/lidarr/lidarr-integration";
|
||||
@@ -28,14 +27,17 @@ export type { IntegrationInput } from "./base/integration";
|
||||
export type { DownloadClientJobsAndStatus } from "./interfaces/downloads/download-client-data";
|
||||
export type { ExtendedDownloadClientItem } from "./interfaces/downloads/download-client-items";
|
||||
export type { ExtendedClientStatus } from "./interfaces/downloads/download-client-status";
|
||||
export type { HealthMonitoring } from "./interfaces/health-monitoring/healt-monitoring";
|
||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request";
|
||||
export type { StreamSession } from "./interfaces/media-server/session";
|
||||
export type { TdarrQueue } from "./interfaces/media-transcoding/queue";
|
||||
export type { TdarrPieSegment, TdarrStatistics } from "./interfaces/media-transcoding/statistics";
|
||||
export type { TdarrWorker } from "./interfaces/media-transcoding/workers";
|
||||
export type { Notification } from "./interfaces/notifications/notification";
|
||||
export type { SystemHealthMonitoring } from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export { MediaRequestStatus } from "./interfaces/media-requests/media-request-types";
|
||||
export type { MediaRequestList, MediaRequestStats } from "./interfaces/media-requests/media-request-types";
|
||||
export type { StreamSession } from "./interfaces/media-server/media-server-types";
|
||||
export type {
|
||||
TdarrQueue,
|
||||
TdarrPieSegment,
|
||||
TdarrStatistics,
|
||||
TdarrWorker,
|
||||
} from "./interfaces/media-transcoding/media-transcoding-types";
|
||||
export type { Notification } from "./interfaces/notifications/notification-types";
|
||||
|
||||
// Schemas
|
||||
export { downloadClientItemSchema } from "./interfaces/downloads/download-client-items";
|
||||
|
||||
@@ -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 { DownloadClientItem } from "./download-client-items";
|
||||
|
||||
export abstract class DownloadClientIntegration extends Integration {
|
||||
export interface IDownloadClientIntegration {
|
||||
/** Get download client's status and list of all of it's items */
|
||||
public abstract getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
||||
getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus>;
|
||||
/** 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 */
|
||||
public abstract pauseItemAsync(item: DownloadClientItem): Promise<void>;
|
||||
pauseItemAsync(item: DownloadClientItem): Promise<void>;
|
||||
/** 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 */
|
||||
public abstract resumeItemAsync(item: DownloadClientItem): Promise<void>;
|
||||
resumeItemAsync(item: DownloadClientItem): Promise<void>;
|
||||
/** 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;
|
||||
cpuModelName: string;
|
||||
cpuUtilization: number;
|
||||
@@ -25,3 +27,11 @@ export interface HealthMonitoring {
|
||||
overallStatus: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
// TODO: in the future decouple this from the Proxmox integration
|
||||
export interface ClusterHealthMonitoring {
|
||||
nodes: NodeResource[];
|
||||
lxcs: LxcResource[];
|
||||
vms: QemuResource[];
|
||||
storages: StorageResource[];
|
||||
}
|
||||
@@ -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 {
|
||||
id: number;
|
||||
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 {
|
||||
name: string;
|
||||
value: number;
|
||||
@@ -21,3 +38,17 @@ export interface TdarrStatistics {
|
||||
audioCodecs: TdarrPieSegment[];
|
||||
audioContainers: TdarrPieSegment[];
|
||||
}
|
||||
|
||||
export interface TdarrWorker {
|
||||
id: string;
|
||||
filePath: string;
|
||||
fps: number;
|
||||
percentage: number;
|
||||
ETA: string;
|
||||
jobType: string;
|
||||
status: string;
|
||||
step: string;
|
||||
originalSize: number;
|
||||
estimatedSize: number | null;
|
||||
outputSize: number | null;
|
||||
}
|
||||
@@ -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";
|
||||
import type { Notification } from "./notification-types";
|
||||
|
||||
export abstract class NotificationsIntegration extends Integration {
|
||||
public abstract getNotificationsAsync(): Promise<Notification[]>;
|
||||
export interface INotificationsIntegration {
|
||||
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 { Integration } from "../base/integration";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
|
||||
|
||||
@HandleIntegrationErrors([integrationAxiosHttpErrorHandler])
|
||||
export class JellyfinIntegration extends Integration {
|
||||
export class JellyfinIntegration extends Integration implements IMediaServerIntegration {
|
||||
private readonly jellyfin: Jellyfin = new Jellyfin({
|
||||
clientInfo: {
|
||||
name: "Homarr",
|
||||
|
||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { CalendarEvent } from "../../calendar-types";
|
||||
import { MediaOrganizerIntegration } from "../media-organizer-integration";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class LidarrIntegration extends MediaOrganizerIntegration {
|
||||
export class LidarrIntegration extends Integration implements ICalendarIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
@@ -103,7 +105,8 @@ export class LidarrIntegration extends MediaOrganizerIntegration {
|
||||
const flatImages = [...event.images];
|
||||
|
||||
const sortedImages = flatImages.sort(
|
||||
(imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType),
|
||||
(imageA, imageB) =>
|
||||
mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType),
|
||||
);
|
||||
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||
return sortedImages[0];
|
||||
|
||||
@@ -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 { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { CalendarEvent } from "../../calendar-types";
|
||||
import { radarrReleaseTypes } from "../../calendar-types";
|
||||
import { MediaOrganizerIntegration } from "../media-organizer-integration";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import { radarrReleaseTypes } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class RadarrIntegration extends MediaOrganizerIntegration {
|
||||
export class RadarrIntegration extends Integration implements ICalendarIntegration {
|
||||
/**
|
||||
* Gets the events in the Radarr calendar between two dates.
|
||||
* @param start The start date
|
||||
@@ -82,7 +84,8 @@ export class RadarrIntegration extends MediaOrganizerIntegration {
|
||||
const flatImages = [...event.images];
|
||||
|
||||
const sortedImages = flatImages.sort(
|
||||
(imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType),
|
||||
(imageA, imageB) =>
|
||||
mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType),
|
||||
);
|
||||
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||
return sortedImages[0];
|
||||
|
||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { CalendarEvent } from "../../calendar-types";
|
||||
import { MediaOrganizerIntegration } from "../media-organizer-integration";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class ReadarrIntegration extends MediaOrganizerIntegration {
|
||||
export class ReadarrIntegration extends Integration implements ICalendarIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api"), {
|
||||
headers: { "X-Api-Key": super.getSecretValue("apiKey") },
|
||||
@@ -81,7 +83,8 @@ export class ReadarrIntegration extends MediaOrganizerIntegration {
|
||||
const flatImages = [...event.images];
|
||||
|
||||
const sortedImages = flatImages.sort(
|
||||
(imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType),
|
||||
(imageA, imageB) =>
|
||||
mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType),
|
||||
);
|
||||
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||
return sortedImages[0];
|
||||
|
||||
@@ -3,13 +3,15 @@ import { z } from "zod";
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { logger } from "@homarr/log";
|
||||
|
||||
import { Integration } from "../../base/integration";
|
||||
import type { IntegrationTestingInput } from "../../base/integration";
|
||||
import { TestConnectionError } from "../../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../../base/test-connection/test-connection-service";
|
||||
import type { CalendarEvent } from "../../calendar-types";
|
||||
import { MediaOrganizerIntegration } from "../media-organizer-integration";
|
||||
import type { ICalendarIntegration } from "../../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../../interfaces/calendar/calendar-types";
|
||||
import { mediaOrganizerPriorities } from "../media-organizer";
|
||||
|
||||
export class SonarrIntegration extends MediaOrganizerIntegration {
|
||||
export class SonarrIntegration extends Integration implements ICalendarIntegration {
|
||||
/**
|
||||
* Gets the events in the Sonarr calendar between two dates.
|
||||
* @param start The start date
|
||||
@@ -81,7 +83,8 @@ export class SonarrIntegration extends MediaOrganizerIntegration {
|
||||
const flatImages = [...event.images, ...event.series.images];
|
||||
|
||||
const sortedImages = flatImages.sort(
|
||||
(imageA, imageB) => this.priorities.indexOf(imageA.coverType) - this.priorities.indexOf(imageB.coverType),
|
||||
(imageA, imageB) =>
|
||||
mediaOrganizerPriorities.indexOf(imageA.coverType) - mediaOrganizerPriorities.indexOf(imageB.coverType),
|
||||
);
|
||||
logger.debug(`Sorted images to [${sortedImages.map((image) => image.coverType).join(",")}]`);
|
||||
return sortedImages[0];
|
||||
|
||||
@@ -4,12 +4,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { TdarrQueue } from "../interfaces/media-transcoding/queue";
|
||||
import type { TdarrStatistics } from "../interfaces/media-transcoding/statistics";
|
||||
import type { TdarrWorker } from "../interfaces/media-transcoding/workers";
|
||||
import type { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration";
|
||||
import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "../interfaces/media-transcoding/media-transcoding-types";
|
||||
import { getNodesResponseSchema, getStatisticsSchema, getStatusTableSchema } from "./tdarr-validation-schemas";
|
||||
|
||||
export class TdarrIntegration extends Integration {
|
||||
export class TdarrIntegration extends Integration implements IMediaTranscodingIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/v2/is-server-alive"), {
|
||||
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 { Integration } from "../base/integration";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { CalendarEvent } from "../calendar-types";
|
||||
import type { ICalendarIntegration } from "../interfaces/calendar/calendar-integration";
|
||||
import type { CalendarEvent } from "../interfaces/calendar/calendar-types";
|
||||
|
||||
@HandleIntegrationErrors([integrationTsdavHttpErrorHandler])
|
||||
export class NextcloudIntegration extends Integration {
|
||||
export class NextcloudIntegration extends Integration implements ICalendarIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const client = await this.createCalendarClientAsync(input.dispatcher);
|
||||
await client.login();
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
|
||||
import { Integration } from "../base/integration";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { Notification } from "../interfaces/notifications/notification";
|
||||
import { NotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
||||
import type { Notification } from "../interfaces/notifications/notification-types";
|
||||
import type { INotificationsIntegration } from "../interfaces/notifications/notifications-integration";
|
||||
import { ntfyNotificationSchema } from "./ntfy-schema";
|
||||
|
||||
export class NTFYIntegration extends NotificationsIntegration {
|
||||
export class NTFYIntegration extends Integration implements INotificationsIntegration {
|
||||
public async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
await input.fetchAsync(this.url("/v1/account"), { headers: this.getHeaders() });
|
||||
return { success: true };
|
||||
|
||||
@@ -9,7 +9,8 @@ import { Integration } from "../base/integration";
|
||||
import type { SessionStore } from "../base/session-store";
|
||||
import { createSessionStore } from "../base/session-store";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { HealthMonitoring } from "../types";
|
||||
import type { ISystemHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
|
||||
import type { SystemHealthMonitoring } from "../types";
|
||||
import { cpuTempSchema, fileSystemSchema, smartSchema, systemInformationSchema } from "./openmediavault-types";
|
||||
|
||||
const localLogger = logger.child({ module: "OpenMediaVaultIntegration" });
|
||||
@@ -18,7 +19,7 @@ type SessionStoreValue =
|
||||
| { type: "header"; sessionId: string }
|
||||
| { type: "cookie"; loginToken: string; sessionId: string };
|
||||
|
||||
export class OpenMediaVaultIntegration extends Integration {
|
||||
export class OpenMediaVaultIntegration extends Integration implements ISystemHealthMonitoringIntegration {
|
||||
private readonly sessionStore: SessionStore<SessionStoreValue>;
|
||||
|
||||
constructor(integration: IntegrationInput) {
|
||||
@@ -26,7 +27,7 @@ export class OpenMediaVaultIntegration extends Integration {
|
||||
this.sessionStore = createSessionStore(integration);
|
||||
}
|
||||
|
||||
public async getSystemInfoAsync(): Promise<HealthMonitoring> {
|
||||
public async getSystemInfoAsync(): Promise<SystemHealthMonitoring> {
|
||||
const systemResponses = await this.makeAuthenticatedRpcCallAsync("system", "getInformation");
|
||||
const fileSystemResponse = await this.makeAuthenticatedRpcCallAsync(
|
||||
"filesystemmgmt",
|
||||
|
||||
@@ -8,8 +8,9 @@ import { Integration } from "../base/integration";
|
||||
import type { ISearchableIntegration } from "../base/searchable-integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request";
|
||||
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request";
|
||||
import type { IMediaRequestIntegration } from "../interfaces/media-requests/media-request-integration";
|
||||
import type { MediaRequest, RequestStats, RequestUser } from "../interfaces/media-requests/media-request-types";
|
||||
import { MediaAvailability, MediaRequestStatus } from "../interfaces/media-requests/media-request-types";
|
||||
|
||||
interface OverseerrSearchResult {
|
||||
id: number;
|
||||
@@ -23,7 +24,10 @@ interface OverseerrSearchResult {
|
||||
/**
|
||||
* Overseerr Integration. See https://api-docs.overseerr.dev
|
||||
*/
|
||||
export class OverseerrIntegration extends Integration implements ISearchableIntegration<OverseerrSearchResult> {
|
||||
export class OverseerrIntegration
|
||||
extends Integration
|
||||
implements IMediaRequestIntegration, ISearchableIntegration<OverseerrSearchResult>
|
||||
{
|
||||
public async searchAsync(query: string) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/search", { query }), {
|
||||
headers: {
|
||||
|
||||
@@ -8,10 +8,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/session";
|
||||
import type { IMediaServerIntegration } from "../interfaces/media-server/media-server-integration";
|
||||
import type { CurrentSessionsInput, StreamSession } from "../interfaces/media-server/media-server-types";
|
||||
import type { PlexResponse } from "./interface";
|
||||
|
||||
export class PlexIntegration extends Integration {
|
||||
export class PlexIntegration extends Integration implements IMediaServerIntegration {
|
||||
public async getCurrentSessionsAsync(_options: CurrentSessionsInput): Promise<StreamSession[]> {
|
||||
const token = super.getSecretValue("apiKey");
|
||||
|
||||
|
||||
@@ -6,10 +6,11 @@ import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { Indexer } from "../interfaces/indexer-manager/indexer";
|
||||
import type { IIndexerManagerIntegration } from "../interfaces/indexer-manager/indexer-manager-integration";
|
||||
import type { Indexer } from "../interfaces/indexer-manager/indexer-manager-types";
|
||||
import { indexerResponseSchema, statusResponseSchema } from "./prowlarr-types";
|
||||
|
||||
export class ProwlarrIntegration extends Integration {
|
||||
export class ProwlarrIntegration extends Integration implements IIndexerManagerIntegration {
|
||||
public async getIndexersAsync(): Promise<Indexer[]> {
|
||||
const apiKey = super.getSecretValue("apiKey");
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import { HandleIntegrationErrors } from "../base/errors/decorator";
|
||||
import type { IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import type { IClusterHealthMonitoringIntegration } from "../interfaces/health-monitoring/health-monitoring-integration";
|
||||
import { ProxmoxApiErrorHandler } from "./proxmox-error-handler";
|
||||
import type {
|
||||
ComputeResourceBase,
|
||||
@@ -19,7 +20,7 @@ import type {
|
||||
} from "./proxmox-types";
|
||||
|
||||
@HandleIntegrationErrors([new ProxmoxApiErrorHandler()])
|
||||
export class ProxmoxIntegration extends Integration {
|
||||
export class ProxmoxIntegration extends Integration implements IClusterHealthMonitoringIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const proxmox = this.getPromoxApi(input.fetchAsync);
|
||||
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/network-controller-summary/network-controller-summary-types";
|
||||
export * from "./interfaces/health-monitoring/healt-monitoring";
|
||||
export * from "./interfaces/indexer-manager/indexer";
|
||||
export * from "./interfaces/media-requests/media-request";
|
||||
export * from "./interfaces/health-monitoring/health-monitoring-types";
|
||||
export * from "./interfaces/indexer-manager/indexer-manager-types";
|
||||
export * from "./interfaces/media-requests/media-request-types";
|
||||
export * from "./base/searchable-integration";
|
||||
export * from "./homeassistant/homeassistant-types";
|
||||
export * from "./proxmox/proxmox-types";
|
||||
|
||||
@@ -2,12 +2,12 @@ import dayjs from "dayjs";
|
||||
|
||||
import type { IntegrationKindByCategory } from "@homarr/definitions";
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { HealthMonitoring, ProxmoxClusterInfo } from "@homarr/integrations/types";
|
||||
import type { ProxmoxClusterInfo, SystemHealthMonitoring } from "@homarr/integrations/types";
|
||||
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
HealthMonitoring,
|
||||
SystemHealthMonitoring,
|
||||
Exclude<IntegrationKindByCategory<"healthMonitoring">, "proxmox">,
|
||||
Record<string, never>
|
||||
>({
|
||||
@@ -21,7 +21,7 @@ export const systemInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
|
||||
export const clusterInfoRequestHandler = createCachedIntegrationRequestHandler<
|
||||
ProxmoxClusterInfo,
|
||||
"proxmox",
|
||||
"proxmox" | "mock",
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration, _input) {
|
||||
|
||||
@@ -5,6 +5,7 @@ import dayjs from "dayjs";
|
||||
import duration from "dayjs/plugin/duration";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import type { IntegrationKind } from "@homarr/definitions";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
@@ -13,21 +14,25 @@ import { SystemHealthMonitoring } from "./system-health";
|
||||
|
||||
dayjs.extend(duration);
|
||||
|
||||
const isClusterIntegration = (integration: { kind: IntegrationKind }) =>
|
||||
integration.kind === "proxmox" || integration.kind === "mock";
|
||||
|
||||
export default function HealthMonitoringWidget(props: WidgetComponentProps<"healthMonitoring">) {
|
||||
const [integrations] = clientApi.integration.byIds.useSuspenseQuery(props.integrationIds);
|
||||
const t = useI18n();
|
||||
|
||||
const proxmoxIntegrationId = integrations.find((integration) => integration.kind === "proxmox")?.id;
|
||||
const clusterIntegrationId = integrations.find(isClusterIntegration)?.id;
|
||||
|
||||
if (!proxmoxIntegrationId) {
|
||||
if (!clusterIntegrationId) {
|
||||
return <SystemHealthMonitoring {...props} />;
|
||||
}
|
||||
|
||||
const otherIntegrationIds = integrations
|
||||
// We want to have the mock integration also in the system tab, so we use it for both
|
||||
.filter((integration) => integration.kind !== "proxmox")
|
||||
.map((integration) => integration.id);
|
||||
if (otherIntegrationIds.length === 0) {
|
||||
return <ClusterHealthMonitoring {...props} integrationId={proxmoxIntegrationId} />;
|
||||
return <ClusterHealthMonitoring {...props} integrationId={clusterIntegrationId} />;
|
||||
}
|
||||
|
||||
return (
|
||||
@@ -45,7 +50,7 @@ export default function HealthMonitoringWidget(props: WidgetComponentProps<"heal
|
||||
<SystemHealthMonitoring {...props} integrationIds={otherIntegrationIds} />
|
||||
</Tabs.Panel>
|
||||
<Tabs.Panel value="cluster">
|
||||
<ClusterHealthMonitoring integrationId={proxmoxIntegrationId} {...props} />
|
||||
<ClusterHealthMonitoring integrationId={clusterIntegrationId} {...props} />
|
||||
</Tabs.Panel>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { IconVideo } from "@tabler/icons-react";
|
||||
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
@@ -10,5 +12,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaServ
|
||||
showOnlyPlaying: factory.switch({ defaultValue: true, withDescription: true }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["jellyfin", "plex", "emby"],
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaService"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IconTransform } from "@tabler/icons-react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { capitalize } from "@homarr/common";
|
||||
import { getIntegrationKindsByCategory } from "@homarr/definitions";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
@@ -19,5 +20,5 @@ export const { componentLoader, definition } = createWidgetDefinition("mediaTran
|
||||
queuePageSize: factory.number({ defaultValue: 10, validate: z.number().min(1).max(30) }),
|
||||
}));
|
||||
},
|
||||
supportedIntegrations: ["tdarr"],
|
||||
supportedIntegrations: getIntegrationKindsByCategory("mediaTranscoding"),
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -124,6 +124,9 @@ importers:
|
||||
'@homarr/docker':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../../packages/docker
|
||||
'@homarr/env':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../../packages/env
|
||||
'@homarr/form':
|
||||
specifier: workspace:^0.1.0
|
||||
version: link:../../packages/form
|
||||
|
||||
Reference in New Issue
Block a user