Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0513a8e6f5 |
No files matched your search
@@ -1,13 +1,16 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { Card, Group, SimpleGrid, Space, Stack, Text } from "@mantine/core";
|
import { Card, Group, SimpleGrid, Space, Stack, Text, Title } from "@mantine/core";
|
||||||
import { IconArrowRight } from "@tabler/icons-react";
|
import { IconArrowRight } from "@tabler/icons-react";
|
||||||
|
|
||||||
import { api } from "@homarr/api/server";
|
import { api } from "@homarr/api/server";
|
||||||
import { auth } from "@homarr/auth/next";
|
import { auth } from "@homarr/auth/next";
|
||||||
import { isProviderEnabled } from "@homarr/auth/server";
|
import { isProviderEnabled } from "@homarr/auth/server";
|
||||||
|
import { getIntegrationName } from "@homarr/definitions";
|
||||||
import { getScopedI18n } from "@homarr/translation/server";
|
import { getScopedI18n } from "@homarr/translation/server";
|
||||||
|
import { IntegrationAvatar } from "@homarr/ui";
|
||||||
|
|
||||||
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
import { DynamicBreadcrumb } from "~/components/navigation/dynamic-breadcrumb";
|
||||||
|
import { catchTrpcNotFound } from "~/errors/trpc-catch-error";
|
||||||
import { createMetaTitle } from "~/metadata";
|
import { createMetaTitle } from "~/metadata";
|
||||||
import { HeroBanner } from "./_components/hero-banner";
|
import { HeroBanner } from "./_components/hero-banner";
|
||||||
|
|
||||||
@@ -30,6 +33,8 @@ export async function generateMetadata() {
|
|||||||
export default async function ManagementPage() {
|
export default async function ManagementPage() {
|
||||||
const statistics = await api.home.getStats();
|
const statistics = await api.home.getStats();
|
||||||
const session = await auth();
|
const session = await auth();
|
||||||
|
const isAdmin = session?.user.permissions.includes("admin") ?? false;
|
||||||
|
const suggestions = isAdmin ? await api.integration.suggestedIntegrations().catch(catchTrpcNotFound) : [];
|
||||||
const t = await getScopedI18n("management.page.home");
|
const t = await getScopedI18n("management.page.home");
|
||||||
|
|
||||||
const links: LinkProps[] = [
|
const links: LinkProps[] = [
|
||||||
@@ -102,6 +107,37 @@ export default async function ManagementPage() {
|
|||||||
),
|
),
|
||||||
)}
|
)}
|
||||||
</SimpleGrid>
|
</SimpleGrid>
|
||||||
|
{suggestions.length >= 1 && (
|
||||||
|
<>
|
||||||
|
<Space h="md" />
|
||||||
|
<Stack gap="sm">
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Title order={3}>Suggested integrations</Title>
|
||||||
|
<Text c="dimmed">Based on your Docker containers</Text>
|
||||||
|
</Stack>
|
||||||
|
<SimpleGrid cols={{ xs: 1, sm: 2, md: 3 }}>
|
||||||
|
{suggestions.slice(0, 6).map((suggestion) => (
|
||||||
|
<Card key={suggestion.kind} component={Link} href="#" radius="lg">
|
||||||
|
<Group justify="space-between">
|
||||||
|
<Group>
|
||||||
|
<IntegrationAvatar kind={suggestion.kind} size="sm" />
|
||||||
|
<Stack gap={0}>
|
||||||
|
<Text size="md" fw={500}>
|
||||||
|
{getIntegrationName(suggestion.kind)}
|
||||||
|
</Text>
|
||||||
|
<Text c="dimmed" size="sm">
|
||||||
|
Show active media streams
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
</Group>
|
||||||
|
<IconArrowRight />
|
||||||
|
</Group>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</SimpleGrid>
|
||||||
|
</Stack>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -14,15 +14,19 @@ import {
|
|||||||
integrationUserPermissions,
|
integrationUserPermissions,
|
||||||
searchEngines,
|
searchEngines,
|
||||||
} from "@homarr/db/schema";
|
} from "@homarr/db/schema";
|
||||||
import type { IntegrationSecretKind } from "@homarr/definitions";
|
import type { IntegrationKind, IntegrationSecretKind } from "@homarr/definitions";
|
||||||
import {
|
import {
|
||||||
getIconUrl,
|
getIconUrl,
|
||||||
getIntegrationKindsByCategory,
|
getIntegrationKindsByCategory,
|
||||||
|
getIntegrationName,
|
||||||
getPermissionsWithParents,
|
getPermissionsWithParents,
|
||||||
integrationDefs,
|
integrationDefs,
|
||||||
integrationKinds,
|
integrationKinds,
|
||||||
integrationSecretKindObject,
|
integrationSecretKindObject,
|
||||||
} from "@homarr/definitions";
|
} from "@homarr/definitions";
|
||||||
|
import type { ContainerInfo, Port } from "@homarr/docker";
|
||||||
|
import { DockerSingleton } from "@homarr/docker";
|
||||||
|
import { env } from "@homarr/docker/env";
|
||||||
import { createIntegrationAsync } from "@homarr/integrations";
|
import { createIntegrationAsync } from "@homarr/integrations";
|
||||||
import { logger } from "@homarr/log";
|
import { logger } from "@homarr/log";
|
||||||
import { byIdSchema } from "@homarr/validation/common";
|
import { byIdSchema } from "@homarr/validation/common";
|
||||||
@@ -527,8 +531,169 @@ export const integrationRouter = createTRPCRouter({
|
|||||||
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
const integrationInstance = await createIntegrationAsync(ctx.integration);
|
||||||
return await integrationInstance.searchAsync(encodeURI(input.query));
|
return await integrationInstance.searchAsync(encodeURI(input.query));
|
||||||
}),
|
}),
|
||||||
|
suggestedIntegrations: permissionRequiredProcedure.requiresPermission("admin").query(async ({ ctx }) => {
|
||||||
|
if (!env.ENABLE_DOCKER)
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Docker is not enabled",
|
||||||
|
});
|
||||||
|
|
||||||
|
const dockerInstances = DockerSingleton.getInstances();
|
||||||
|
if (dockerInstances.length === 0) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "No Docker instances found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: remove those that were already added
|
||||||
|
const containers = await Promise.all(
|
||||||
|
dockerInstances.map(({ instance, host, isSocket }) =>
|
||||||
|
instance.listContainers().then((containers) =>
|
||||||
|
containers.map((containerInfo) => ({
|
||||||
|
info: containerInfo,
|
||||||
|
host,
|
||||||
|
isSocket,
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
).then((results) => results.flat());
|
||||||
|
|
||||||
|
// Default docker hostname consist out of first 12 characters of the container id
|
||||||
|
const homarrContainer = containers.find((container) => container.info.Id.slice(0, 12) === process.env.HOSTNAME);
|
||||||
|
|
||||||
|
const suggestions = containers
|
||||||
|
.map((container) => getSuggestionFromContainer(container, homarrContainer))
|
||||||
|
.filter((suggestion) => suggestion !== null);
|
||||||
|
|
||||||
|
const groupedSuggestions = suggestions.reduce(
|
||||||
|
(acc, suggestion) => {
|
||||||
|
acc[suggestion.kind] ??= [];
|
||||||
|
// is defined above
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||||
|
acc[suggestion.kind]!.push(suggestion);
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Partial<Record<IntegrationKind, IntegrationSuggestion[]>>,
|
||||||
|
);
|
||||||
|
|
||||||
|
return Object.entries(groupedSuggestions)
|
||||||
|
.map(([kind, suggestions]) => ({
|
||||||
|
kind: kind as IntegrationKind,
|
||||||
|
suggestions,
|
||||||
|
}))
|
||||||
|
.sort((itemA, itemB) => {
|
||||||
|
if (itemA.suggestions.length !== itemB.suggestions.length)
|
||||||
|
return itemB.suggestions.length - itemA.suggestions.length;
|
||||||
|
|
||||||
|
const createdAtA = Math.max(...itemA.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
|
||||||
|
const createdAtB = Math.max(...itemB.suggestions.map((suggestion) => suggestion.createdAt?.getTime() ?? 0));
|
||||||
|
return createdAtB - createdAtA;
|
||||||
|
});
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
interface IntegrationSuggestion {
|
||||||
|
containerId: string;
|
||||||
|
name: string;
|
||||||
|
internalUrl: string | null;
|
||||||
|
externalUrl: string | null;
|
||||||
|
kind: IntegrationKind;
|
||||||
|
createdAt?: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ContainerInfoWithHost {
|
||||||
|
info: ContainerInfo;
|
||||||
|
host: string;
|
||||||
|
isSocket: boolean | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const getSuggestionFromContainer = (
|
||||||
|
container: ContainerInfoWithHost,
|
||||||
|
homarrContainer: ContainerInfoWithHost | undefined,
|
||||||
|
): IntegrationSuggestion | null => {
|
||||||
|
const integrationKind = getSuggestionKind(container.info);
|
||||||
|
|
||||||
|
if (!integrationKind) return null;
|
||||||
|
|
||||||
|
return {
|
||||||
|
containerId: container.info.Id,
|
||||||
|
name: getIntegrationName(integrationKind),
|
||||||
|
kind: integrationKind,
|
||||||
|
internalUrl: getInternalUrl(container, homarrContainer),
|
||||||
|
externalUrl: getExternalUrl(container),
|
||||||
|
createdAt: container.info.Created ? new Date(container.info.Created * 1000) : undefined,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const getSuggestionKind = (container: ContainerInfo): IntegrationKind | null => {
|
||||||
|
const [image, _tag] = container.Image.split(":");
|
||||||
|
if (!image) return null;
|
||||||
|
return (
|
||||||
|
objectEntries(integrationDefs).find(
|
||||||
|
([, integration]) =>
|
||||||
|
"containerImages" in integration &&
|
||||||
|
integration.containerImages.some((containerImage) => containerImage.toLowerCase() === image.toLowerCase()),
|
||||||
|
)?.[0] ?? null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePortScore = (port: Port): number => {
|
||||||
|
let score = 0;
|
||||||
|
|
||||||
|
if (port.PublicPort) score += 10; // Public ports are more likely to be used
|
||||||
|
if (port.PrivatePort % 1000 === 80) score += 5; // Default http ports are most likely suffixed with 80 like 8080 or 80
|
||||||
|
if (port.PrivatePort % 1000 === 443) score += 10; // Default https ports are most likely suffixed with 443 like 8443 or 443
|
||||||
|
return score;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPort = (container: ContainerInfoWithHost): Port | null => {
|
||||||
|
const ports = container.info.Ports.filter((port) => port.Type === "tcp");
|
||||||
|
return (
|
||||||
|
ports
|
||||||
|
.map((port) => ({ port, score: calculatePortScore(port) }))
|
||||||
|
.sort((portA, portB) => portB.score - portA.score)
|
||||||
|
.at(0)?.port ?? null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getInternalUrl = (
|
||||||
|
container: ContainerInfoWithHost,
|
||||||
|
homarrContainer: ContainerInfoWithHost | undefined,
|
||||||
|
): string | null => {
|
||||||
|
const port = getPort(container);
|
||||||
|
if (!port) return null;
|
||||||
|
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
|
||||||
|
|
||||||
|
if (homarrContainer) {
|
||||||
|
const matchingNetwork = Object.entries(homarrContainer.info.NetworkSettings.Networks).find(
|
||||||
|
([_key, homarrNetwork]) =>
|
||||||
|
Object.entries(container.info.NetworkSettings.Networks).some(
|
||||||
|
([_name, network]) => homarrNetwork.NetworkID === network.NetworkID,
|
||||||
|
),
|
||||||
|
)?.[1];
|
||||||
|
if (matchingNetwork) return `${protocol}://${matchingNetwork.IPAddress}:${port.PrivatePort}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Let user specify the internal ip-address for integrations --> use as default before falling back to host.docker.internal
|
||||||
|
if (container.host === homarrContainer?.host && container.isSocket === homarrContainer.isSocket) {
|
||||||
|
return `${protocol}://host.docker.internal:${port.PublicPort}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Change this
|
||||||
|
return `${protocol}://localhost:${port.PublicPort}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getExternalUrl = (container: ContainerInfoWithHost): string | null => {
|
||||||
|
const port = getPort(container);
|
||||||
|
if (!port) return null;
|
||||||
|
const protocol = port.PrivatePort % 1000 === 443 ? "https" : "http";
|
||||||
|
|
||||||
|
// TODO: Use some labels like "traefik.host" to determine the external url
|
||||||
|
// For now we just use localhost as external url
|
||||||
|
return `${protocol}://localhost:${port.PublicPort}`;
|
||||||
|
};
|
||||||
|
|
||||||
interface UpdateSecretInput {
|
interface UpdateSecretInput {
|
||||||
integrationId: string;
|
integrationId: string;
|
||||||
value: string;
|
value: string;
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ interface integrationDefinition {
|
|||||||
iconUrl: string;
|
iconUrl: string;
|
||||||
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
|
secretKinds: AtLeastOneOf<IntegrationSecretKind[]>; // at least one secret kind set is required
|
||||||
category: AtLeastOneOf<IntegrationCategory>;
|
category: AtLeastOneOf<IntegrationCategory>;
|
||||||
|
containerImages?: string[]; // Optional, used for container images to suggest in the UI
|
||||||
}
|
}
|
||||||
|
|
||||||
export const integrationDefs = {
|
export const integrationDefs = {
|
||||||
@@ -24,30 +25,45 @@ export const integrationDefs = {
|
|||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sabnzbd.svg",
|
||||||
category: ["downloadClient", "usenet"],
|
category: ["downloadClient", "usenet"],
|
||||||
|
containerImages: ["linuxserver/sabnzbd", "lscr.io/linuxserver/sabnzbd", "ghcr.io/linuxserver/sabnzbd"],
|
||||||
},
|
},
|
||||||
nzbGet: {
|
nzbGet: {
|
||||||
name: "NZBGet",
|
name: "NZBGet",
|
||||||
secretKinds: [["username", "password"]],
|
secretKinds: [["username", "password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/nzbget.svg",
|
||||||
category: ["downloadClient", "usenet"],
|
category: ["downloadClient", "usenet"],
|
||||||
|
containerImages: [
|
||||||
|
"nzbgetcom/nzbget",
|
||||||
|
"ghcr.io/nzbgetcom/nzbget",
|
||||||
|
"linuxserver/nzbget",
|
||||||
|
"lscr.io/linuxserver/nzbget",
|
||||||
|
"ghcr.io/linuxserver/nzbget",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
deluge: {
|
deluge: {
|
||||||
name: "Deluge",
|
name: "Deluge",
|
||||||
secretKinds: [["password"]],
|
secretKinds: [["password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/deluge.svg",
|
||||||
category: ["downloadClient", "torrent"],
|
category: ["downloadClient", "torrent"],
|
||||||
|
containerImages: ["linuxserver/deluge", "lscr.io/linuxserver/deluge", "ghcr.io/linuxserver/deluge"],
|
||||||
},
|
},
|
||||||
transmission: {
|
transmission: {
|
||||||
name: "Transmission",
|
name: "Transmission",
|
||||||
secretKinds: [["username", "password"]],
|
secretKinds: [["username", "password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/transmission.svg",
|
||||||
category: ["downloadClient", "torrent"],
|
category: ["downloadClient", "torrent"],
|
||||||
|
containerImages: [
|
||||||
|
"linuxserver/transmission",
|
||||||
|
"lscr.io/linuxserver/transmission",
|
||||||
|
"ghcr.io/linuxserver/transmission",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
qBittorrent: {
|
qBittorrent: {
|
||||||
name: "qBittorrent",
|
name: "qBittorrent",
|
||||||
secretKinds: [["username", "password"]],
|
secretKinds: [["username", "password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/qbittorrent.svg",
|
||||||
category: ["downloadClient", "torrent"],
|
category: ["downloadClient", "torrent"],
|
||||||
|
containerImages: ["linuxserver/qbittorrent", "lscr.io/linuxserver/qbittorrent", "ghcr.io/linuxserver/qbittorrent"],
|
||||||
},
|
},
|
||||||
aria2: {
|
aria2: {
|
||||||
name: "Aria2",
|
name: "Aria2",
|
||||||
@@ -60,78 +76,144 @@ export const integrationDefs = {
|
|||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/sonarr.svg",
|
||||||
category: ["calendar"],
|
category: ["calendar"],
|
||||||
|
containerImages: [
|
||||||
|
"hotio/sonarr",
|
||||||
|
"ghcr.io/hotio/sonarr",
|
||||||
|
"linuxserver/sonarr",
|
||||||
|
"lscr.io/linuxserver/sonarr",
|
||||||
|
"ghcr.io/linuxserver/sonarr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
radarr: {
|
radarr: {
|
||||||
name: "Radarr",
|
name: "Radarr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/radarr.svg",
|
||||||
category: ["calendar"],
|
category: ["calendar"],
|
||||||
|
containerImages: [
|
||||||
|
"hotio/radarr",
|
||||||
|
"ghcr.io/hotio/radarr",
|
||||||
|
"linuxserver/radarr",
|
||||||
|
"lscr.io/linuxserver/radarr",
|
||||||
|
"ghcr.io/linuxserver/radarr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
lidarr: {
|
lidarr: {
|
||||||
name: "Lidarr",
|
name: "Lidarr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/lidarr.svg",
|
||||||
category: ["calendar"],
|
category: ["calendar"],
|
||||||
|
containerImages: [
|
||||||
|
"hotio/lidarr",
|
||||||
|
"ghcr.io/hotio/lidarr",
|
||||||
|
"linuxserver/lidarr",
|
||||||
|
"lscr.io/linuxserver/lidarr",
|
||||||
|
"ghcr.io/linuxserver/lidarr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
readarr: {
|
readarr: {
|
||||||
name: "Readarr",
|
name: "Readarr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/readarr.svg",
|
||||||
category: ["calendar"],
|
category: ["calendar"],
|
||||||
|
containerImages: [
|
||||||
|
"hotio/readarr",
|
||||||
|
"ghcr.io/hotio/readarr",
|
||||||
|
"linuxserver/readarr",
|
||||||
|
"lscr.io/linuxserver/readarr",
|
||||||
|
"ghcr.io/linuxserver/readarr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
prowlarr: {
|
prowlarr: {
|
||||||
name: "Prowlarr",
|
name: "Prowlarr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/prowlarr.svg",
|
||||||
category: ["indexerManager"],
|
category: ["indexerManager"],
|
||||||
|
containerImages: [
|
||||||
|
"hotio/prowlarr",
|
||||||
|
"ghcr.io/hotio/prowlarr",
|
||||||
|
"linuxserver/prowlarr",
|
||||||
|
"lscr.io/linuxserver/prowlarr",
|
||||||
|
"ghcr.io/linuxserver/prowlarr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
jellyfin: {
|
jellyfin: {
|
||||||
name: "Jellyfin",
|
name: "Jellyfin",
|
||||||
secretKinds: [["username", "password"], ["apiKey"]],
|
secretKinds: [["username", "password"], ["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyfin.svg",
|
||||||
category: ["mediaService"],
|
category: ["mediaService"],
|
||||||
|
containerImages: [
|
||||||
|
"jellyfin/jellyfin",
|
||||||
|
"ghcr.io/jellyfin/jellyfin",
|
||||||
|
"linuxserver/jellyfin",
|
||||||
|
"lscr.io/linuxserver/jellyfin",
|
||||||
|
"ghcr.io/linuxserver/jellyfin",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
emby: {
|
emby: {
|
||||||
name: "Emby",
|
name: "Emby",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/emby.svg",
|
||||||
category: ["mediaService"],
|
category: ["mediaService"],
|
||||||
|
containerImages: [
|
||||||
|
"emby/embyserver",
|
||||||
|
"emby/embyserver_arm64v8",
|
||||||
|
"emby/embyserver_arm32v7",
|
||||||
|
"linuxserver/emby",
|
||||||
|
"lscr.io/linuxserver/emby",
|
||||||
|
"ghcr.io/linuxserver/emby",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
plex: {
|
plex: {
|
||||||
name: "Plex",
|
name: "Plex",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/plex.svg",
|
||||||
category: ["mediaService"],
|
category: ["mediaService"],
|
||||||
|
containerImages: ["plexinc/pms-docker", "linuxserver/plex", "lscr.io/linuxserver/plex", "ghcr.io/linuxserver/plex"],
|
||||||
},
|
},
|
||||||
jellyseerr: {
|
jellyseerr: {
|
||||||
name: "Jellyseerr",
|
name: "Jellyseerr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/jellyseerr.svg",
|
||||||
category: ["mediaSearch", "mediaRequest", "search"],
|
category: ["mediaSearch", "mediaRequest", "search"],
|
||||||
|
containerImages: ["fallenbagel/jellyseerr", "ghcr.io/fallenbagel/jellyseerr"],
|
||||||
},
|
},
|
||||||
overseerr: {
|
overseerr: {
|
||||||
name: "Overseerr",
|
name: "Overseerr",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/overseerr.svg",
|
||||||
category: ["mediaSearch", "mediaRequest", "search"],
|
category: ["mediaSearch", "mediaRequest", "search"],
|
||||||
|
containerImages: [
|
||||||
|
"sct/overseerr",
|
||||||
|
"linuxserver/overseerr",
|
||||||
|
"lscr.io/linuxserver/overseerr",
|
||||||
|
"ghcr.io/linuxserver/overseerr",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
piHole: {
|
piHole: {
|
||||||
name: "Pi-hole",
|
name: "Pi-hole",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/pi-hole.svg",
|
||||||
category: ["dnsHole"],
|
category: ["dnsHole"],
|
||||||
|
containerImages: ["pihole/pihole"],
|
||||||
},
|
},
|
||||||
adGuardHome: {
|
adGuardHome: {
|
||||||
name: "AdGuard Home",
|
name: "AdGuard Home",
|
||||||
secretKinds: [["username", "password"]],
|
secretKinds: [["username", "password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/adguard-home.svg",
|
||||||
category: ["dnsHole"],
|
category: ["dnsHole"],
|
||||||
|
containerImages: ["adguard/adguardhome"],
|
||||||
},
|
},
|
||||||
homeAssistant: {
|
homeAssistant: {
|
||||||
name: "Home Assistant",
|
name: "Home Assistant",
|
||||||
secretKinds: [["apiKey"]],
|
secretKinds: [["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/svg/home-assistant.svg",
|
||||||
category: ["smartHomeServer"],
|
category: ["smartHomeServer"],
|
||||||
|
containerImages: [
|
||||||
|
"homeassistant/home-assistant",
|
||||||
|
"linuxserver/home-assistant",
|
||||||
|
"lscr.io/linuxserver/home-assistant",
|
||||||
|
"ghcr.io/linuxserver/home-assistant",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
openmediavault: {
|
openmediavault: {
|
||||||
name: "OpenMediaVault",
|
name: "OpenMediaVault",
|
||||||
@@ -144,12 +226,14 @@ export const integrationDefs = {
|
|||||||
secretKinds: [[]],
|
secretKinds: [[]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/dashdot.png",
|
||||||
category: ["healthMonitoring"],
|
category: ["healthMonitoring"],
|
||||||
|
containerImages: ["mauricenino/dashdot", "ghcr.io/mauricenino/dashdot"],
|
||||||
},
|
},
|
||||||
tdarr: {
|
tdarr: {
|
||||||
name: "Tdarr",
|
name: "Tdarr",
|
||||||
secretKinds: [[], ["apiKey"]],
|
secretKinds: [[], ["apiKey"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/tdarr.png",
|
||||||
category: ["mediaTranscoding"],
|
category: ["mediaTranscoding"],
|
||||||
|
containerImages: ["haveagitgat/tdarr", "ghcr.io/haveagitgat/tdarr"],
|
||||||
},
|
},
|
||||||
proxmox: {
|
proxmox: {
|
||||||
name: "Proxmox",
|
name: "Proxmox",
|
||||||
@@ -168,6 +252,16 @@ export const integrationDefs = {
|
|||||||
secretKinds: [["username", "password"]],
|
secretKinds: [["username", "password"]],
|
||||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/unifi.png",
|
||||||
category: ["networkController"],
|
category: ["networkController"],
|
||||||
|
containerImages: [
|
||||||
|
"jacobalberty/unifi",
|
||||||
|
"ghcr.io/jacobalberty/unifi",
|
||||||
|
"linuxserver/unifi-controller",
|
||||||
|
"lscr.io/linuxserver/unifi-controller",
|
||||||
|
"ghcr.io/linuxserver/unifi-controller",
|
||||||
|
"linuxserver/unifi-network-application",
|
||||||
|
"lscr.io/linuxserver/unifi-network-application",
|
||||||
|
"ghcr.io/linuxserver/unifi-network-application",
|
||||||
|
],
|
||||||
},
|
},
|
||||||
} as const satisfies Record<string, integrationDefinition>;
|
} as const satisfies Record<string, integrationDefinition>;
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { env } from "./env";
|
|||||||
export interface DockerInstance {
|
export interface DockerInstance {
|
||||||
host: string;
|
host: string;
|
||||||
instance: Docker;
|
instance: Docker;
|
||||||
|
isSocket?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DockerSingleton {
|
export class DockerSingleton {
|
||||||
@@ -14,7 +15,7 @@ export class DockerSingleton {
|
|||||||
const hostVariable = env.DOCKER_HOSTNAMES;
|
const hostVariable = env.DOCKER_HOSTNAMES;
|
||||||
const portVariable = env.DOCKER_PORTS;
|
const portVariable = env.DOCKER_PORTS;
|
||||||
if (hostVariable === undefined || portVariable === undefined) {
|
if (hostVariable === undefined || portVariable === undefined) {
|
||||||
return [{ host: "socket", instance: new Docker() }];
|
return [{ host: "socket", instance: new Docker(), isSocket: true }];
|
||||||
}
|
}
|
||||||
const hostnames = hostVariable.split(",");
|
const hostnames = hostVariable.split(",");
|
||||||
const ports = portVariable.split(",");
|
const ports = portVariable.split(",");
|
||||||
|
|||||||
Reference in new issue
Block a user