Skip to content

Commit

Permalink
feat: initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
SeDemal committed Jul 31, 2024
1 parent b65f3ae commit e133977
Show file tree
Hide file tree
Showing 45 changed files with 2,384 additions and 601 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,7 @@ db.sqlite
*.log

apps/tasks/tasks.cjs
apps/websocket/wssServer.cjs
apps/websocket/wssServer.cjs

#personal backgrounds
apps/nextjs/public/images/background.png
6 changes: 4 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@
"cqmin",
"homarr",
"jellyfin",
"mantine",
"Sabnzbd",
"Sonarr",
"superjson",
"trpc",
"Umami",
"Sonarr"
"Umami"
],
"i18n-ally.dirStructure": "auto",
"i18n-ally.enabledFrameworks": ["next-international"],
Expand Down
4 changes: 4 additions & 0 deletions apps/nextjs/src/app/[locale]/widgets/[kind]/_content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,9 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
});
}, [dimensions, openPreviewDimensionsModal]);

const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) =>
setState({ ...state, options: { ...state.options, newOptions } });

return (
<>
<Card withBorder w={dimensions.width} h={dimensions.height} p={dimensions.height >= 96 ? undefined : 4}>
Expand All @@ -105,6 +108,7 @@ export const WidgetPreviewPageContent = ({ kind, integrationData }: WidgetPrevie
isEditMode={editMode}
boardId={undefined}
itemId={undefined}
setOptions={updateOptions}
/>
</ErrorBoundary>
)}
Expand Down
4 changes: 4 additions & 0 deletions apps/nextjs/src/components/board/sections/content.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,10 @@ const BoardItemContent = ({ item, ...dimensions }: ItemContentProps) => {
const serverData = useServerDataFor(item.id);
const Comp = loadWidgetDynamic(item.kind);
const options = reduceWidgetOptionsWithDefaultValues(item.kind, item.options);
const { updateItemOptions } = useItemActions();
const newItem = { ...item, options };
const updateOptions = ({ newOptions }: { newOptions: Record<string, unknown> }) =>
updateItemOptions({ itemId: item.id, newOptions });

if (!serverData?.isReady) return null;

Expand All @@ -124,6 +127,7 @@ const BoardItemContent = ({ item, ...dimensions }: ItemContentProps) => {
isEditMode={isEditMode}
boardId={board.id}
itemId={item.id}
setOptions={updateOptions}
{...dimensions}
/>
</ErrorBoundary>
Expand Down
10 changes: 6 additions & 4 deletions apps/nextjs/src/components/board/sections/item.module.css
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
.itemCard {
@mixin dark {
background-color: rgba(46, 46, 46, var(--opacity));
border-color: rgba(66, 66, 66, var(--opacity));
--background-color: rgb(from var(--mantine-color-dark-6) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-dark-4) r g b / var(--opacity));
}
@mixin light {
background-color: rgba(255, 255, 255, var(--opacity));
border-color: rgba(222, 226, 230, var(--opacity));
--background-color: rgb(from var(--mantine-color-white) r g b / var(--opacity));
--border-color: rgb(from var(--mantine-color-gray-3) r g b / var(--opacity));
}
background-color: var(--background-color);
border-color: var(--border-color);
}
139 changes: 139 additions & 0 deletions packages/api/src/router/widgets/downloads.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { TRPCError } from "@trpc/server";
import { observable } from "@trpc/server/observable";

import type { IntegrationKind } from "@homarr/definitions";
import type {
DownloadClientData,
DownloadClientIntegration,
IntegrationInput,
SanitizedIntegration,
} from "@homarr/integrations";
import { integrationCreatorByKind } from "@homarr/integrations";
import { createItemAndIntegrationChannel } from "@homarr/redis";
import { z } from "@homarr/validation";

import type { DownloadClientItem } from "../../../../integrations/src/interfaces/downloads/download-client-items";
import { createManyIntegrationMiddleware } from "../../middlewares/integration";
import { createTRPCRouter, publicProcedure } from "../../trpc";

export const downloadsRouter = createTRPCRouter({
getData: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("query", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.query(async ({ ctx }) => {
return await Promise.all(
ctx.integrations.map(async ({ decryptedSecrets: _, ...integration }) => {
const channel = createItemAndIntegrationChannel<DownloadClientData>("downloads", integration.id);
const data = await channel.getAsync();
return {
integration: integration as SanitizedIntegration,
data: data?.data ?? ({} as DownloadClientData),
};
}),
);
}),
subscribeToData: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("query", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.subscription(({ ctx }) => {
return observable<{ integration: SanitizedIntegration; data: DownloadClientData }>((emit) => {
const unsubscribes: (() => void)[] = [];
for (const integrationWithSecrets of ctx.integrations) {
const { decryptedSecrets: _, ...integration } = integrationWithSecrets;
const channel = createItemAndIntegrationChannel<DownloadClientData>("downloads", integration.id);
const unsubscribe = channel.subscribe((sessions) => {
emit.next({
integration: integration as SanitizedIntegration,
data: sessions,
});
});
unsubscribes.push(unsubscribe);
}
return () => {
unsubscribes.forEach((unsubscribe) => {
unsubscribe();
});
};
});
}),
pause: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("interact", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = getIntegrationInstance(integration.kind, integration);
await integrationInstance.pauseQueueAsync();
}),
);
}),
pauseItem: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("interact", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.input(z.object({ item: z.any() satisfies z.ZodType<DownloadClientItem> }))
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = getIntegrationInstance(integration.kind, integration);
await integrationInstance.pauseItemAsync(input.item as DownloadClientItem);
}),
);
}),
resume: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("interact", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.mutation(async ({ ctx }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = getIntegrationInstance(integration.kind, integration);
await integrationInstance.resumeQueueAsync();
}),
);
}),
resumeItem: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("interact", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.input(z.object({ item: z.any() satisfies z.ZodType<DownloadClientItem> }))
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = getIntegrationInstance(integration.kind, integration);
await integrationInstance.resumeItemAsync(input.item as DownloadClientItem);
}),
);
}),
deleteItem: publicProcedure
.unstable_concat(
createManyIntegrationMiddleware("interact", "sabNzbd", "nzbGet", "qBittorrent", "deluge", "transmission"),
)
.input(z.object({ item: z.any() satisfies z.ZodType<DownloadClientItem>, fromDisk: z.boolean() }))
.mutation(async ({ ctx, input }) => {
await Promise.all(
ctx.integrations.map(async (integration) => {
const integrationInstance = getIntegrationInstance(integration.kind, integration);
await integrationInstance.deleteItemAsync(input.item as DownloadClientItem, input.fromDisk);
}),
);
}),
});

function getIntegrationInstance(kind: IntegrationKind, integration: IntegrationInput): DownloadClientIntegration {
switch (kind) {
case "sabNzbd":
case "nzbGet":
case "qBittorrent":
case "deluge":
case "transmission":
return integrationCreatorByKind(kind, integration) as DownloadClientIntegration;
default:
throw new TRPCError({
code: "BAD_REQUEST",
});
}
}
4 changes: 2 additions & 2 deletions packages/api/src/router/widgets/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { createTRPCRouter } from "../../trpc";
import { appRouter } from "./app";
import { calendarRouter } from "./calendar";
import { dnsHoleRouter } from "./dns-hole";
import { downloadsRouter } from "./downloads";
import { mediaServerRouter } from "./media-server";
import { notebookRouter } from "./notebook";
import { smartHomeRouter } from "./smart-home";
import { weatherRouter } from "./weather";
import {usenetDownloadsRouter} from "./usenet-downloads";

export const widgetRouter = createTRPCRouter({
notebook: notebookRouter,
Expand All @@ -16,5 +16,5 @@ export const widgetRouter = createTRPCRouter({
smartHome: smartHomeRouter,
mediaServer: mediaServerRouter,
calendar: calendarRouter,
usenetDownloads: usenetDownloadsRouter
downloads: downloadsRouter,
});
80 changes: 0 additions & 80 deletions packages/api/src/router/widgets/usenet-downloads.ts

This file was deleted.

19 changes: 19 additions & 0 deletions packages/common/src/number.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ const ranges = [
{ divider: 1e3, suffix: "k" },
];

//64bit limit for Number stops at EiB
const siRanges = ["B", "kiB", "MiB", "GiB", "TiB", "PiB", "EiB"];

export const formatNumber = (value: number, decimalPlaces: number) => {
for (const range of ranges) {
if (value < range.divider) continue;
Expand All @@ -19,3 +22,19 @@ export const formatNumber = (value: number, decimalPlaces: number) => {
export const randomInt = (min: number, max: number) => {
return Math.floor(Math.random() * (max - min + 1) + min);
};

/**
* Number of bytes to si format. (division by 1024)
* Does not accept floats, size in bytes are should be integer.
*/
export const humanFileSize = (size: number) => {
if (!Number.isInteger(size)) return NaN;
let count = 0;
while (true) {
const tempSize = size / Math.pow(1024, count);
if (tempSize < 1024 || count === siRanges.length - 1) {
return tempSize.toFixed(Math.min(count, 1)) + siRanges[count];
}
count++;
}
};
4 changes: 2 additions & 2 deletions packages/cron-jobs/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { analyticsJob } from "./jobs/analytics";
import { iconsUpdaterJob } from "./jobs/icons-updater";
import { downloadsJob } from "./jobs/integrations/downloads";
import { smartHomeEntityStateJob } from "./jobs/integrations/home-assistant";
import { mediaOrganizerJob } from "./jobs/integrations/media-organizer";
import { mediaServerJob } from "./jobs/integrations/media-server";
import { pingJob } from "./jobs/ping";
import { createCronJobGroup } from "./lib";
import { usenetDownloadsJob } from "./jobs/integrations/usenet-downloads";

export const jobGroup = createCronJobGroup({
analytics: analyticsJob,
Expand All @@ -14,7 +14,7 @@ export const jobGroup = createCronJobGroup({
smartHomeEntityState: smartHomeEntityStateJob,
mediaServer: mediaServerJob,
mediaOrganizer: mediaOrganizerJob,
usenetDownloads: usenetDownloadsJob
downloads: downloadsJob,
});

export type JobGroupKeys = ReturnType<(typeof jobGroup)["getKeys"]>[number];
Loading

0 comments on commit e133977

Please sign in to comment.