Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db31f880c2 | ||
|
|
b902f93a3c | ||
|
|
973baedfcb | ||
|
|
629f3b0955 |
No files matched your search
@@ -1,36 +0,0 @@
|
||||
---
|
||||
title: 'FileFlows'
|
||||
description: "FileFlows is a distributed media processing system that allows you to encode, compress and process your media files across multiple nodes."
|
||||
hide_title: true
|
||||
---
|
||||
|
||||
import { IntegrationHeader } from '@site/src/components/integrations/header';
|
||||
import { IntegrationCapabilites } from '@site/src/components/integrations/widgets';
|
||||
import { AddingIntegration } from '@site/src/components/integrations/adding';
|
||||
import {IntegrationSecrets} from '@site/src/components/integrations/secrets';
|
||||
import { IconTransform } from "@tabler/icons-react";
|
||||
import { fileFlowsIntegration } from '.';
|
||||
import { mediaTranscodingWidget } from '@site/docs/widgets/media-transcoding';
|
||||
|
||||
|
||||
<IntegrationHeader
|
||||
integration={fileFlowsIntegration}
|
||||
categories={['Media Transcoding']}
|
||||
/>
|
||||
|
||||
### Widgets & Capabilities
|
||||
<IntegrationCapabilites
|
||||
items={[{ widget: mediaTranscodingWidget }]}
|
||||
/>
|
||||
|
||||
### Adding the integration
|
||||
<AddingIntegration />
|
||||
|
||||
### Secrets
|
||||
<IntegrationSecrets secrets={[{
|
||||
credentials: ['apiKey'],
|
||||
steps: []
|
||||
}, {
|
||||
credentials: [],
|
||||
steps: []
|
||||
}]} />
|
||||
@@ -1,8 +0,0 @@
|
||||
import { IntegrationDefinition } from "@site/src/types";
|
||||
export const fileFlowsIntegration: IntegrationDefinition = {
|
||||
name: "FileFlows",
|
||||
description:
|
||||
"FileFlows is a distributed media processing system that allows you to encode, compress and process your media files across multiple nodes.",
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/png/fileflows.png",
|
||||
path: "../../integrations/fileflows",
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
import { createOneIntegrationMiddleware } from "../../middlewares/integration";
|
||||
import { createTRPCRouter, publicProcedure } from "../../trpc";
|
||||
|
||||
export const dawarichRouter = createTRPCRouter({
|
||||
getStatistics: publicProcedure.concat(createOneIntegrationMiddleware("query", "dawarich")).query(async ({ ctx }) => {
|
||||
const { dawarichStatisticsRequestHandler } = await import("@homarr/request-handler/dawarich");
|
||||
const innerHandler = dawarichStatisticsRequestHandler.handler(ctx.integration, {});
|
||||
const data = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data.data;
|
||||
}),
|
||||
|
||||
getPlaces: publicProcedure
|
||||
.concat(createOneIntegrationMiddleware("query", "dawarich"))
|
||||
.input(
|
||||
z.object({
|
||||
limit: z.number().min(1).max(50).default(10),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const { dawarichPlacesRequestHandler } = await import("@homarr/request-handler/dawarich");
|
||||
const innerHandler = dawarichPlacesRequestHandler.handler(ctx.integration, {});
|
||||
const { data } = await innerHandler.getCachedOrUpdatedDataAsync({ forceUpdate: false });
|
||||
return data.slice(0, input.limit);
|
||||
}),
|
||||
});
|
||||
@@ -39,4 +39,5 @@ export const widgetRouter = createTRPCRouter({
|
||||
vpn: lazy(() => import("./vpn").then((mod) => mod.vpnRouter)),
|
||||
ups: lazy(() => import("./ups").then((mod) => mod.upsRouter)),
|
||||
customApi: lazy(() => import("./custom-api").then((mod) => mod.customApiRouter)),
|
||||
dawarich: lazy(() => import("./dawarich").then((mod) => mod.dawarichRouter)),
|
||||
});
|
||||
@@ -36,7 +36,6 @@ const integrationAliases: Partial<Record<IntegrationKind, readonly string[]>> =
|
||||
linuxServerIO: ["linuxserver"],
|
||||
coolify: ["coolify"],
|
||||
truenas: ["truenas"],
|
||||
fileFlows: ["fileflows"],
|
||||
};
|
||||
|
||||
export const matchIntegrationKind = (search: string): IntegrationKind | null => {
|
||||
|
||||
@@ -312,7 +312,6 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/dash-dot"
|
||||
| "/docs/integrations/glances"
|
||||
| "/docs/integrations/tdarr"
|
||||
| "/docs/integrations/fileflows"
|
||||
| "/docs/integrations/proxmox"
|
||||
| "/docs/integrations/nextcloud"
|
||||
| "/docs/integrations/unifi-controller"
|
||||
@@ -344,6 +343,7 @@ export type HomarrDocumentationPath =
|
||||
| "/docs/integrations/gluetun"
|
||||
| "/docs/integrations/peanut"
|
||||
| "/docs/integrations/beszel"
|
||||
| "/docs/integrations/dawarich"
|
||||
| "/docs/widgets/clock"
|
||||
| "/docs/widgets/weather"
|
||||
| "/docs/widgets/app"
|
||||
|
||||
@@ -27,7 +27,6 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
|
||||
dashDot: "dash-dot",
|
||||
glances: "glances",
|
||||
tdarr: "tdarr",
|
||||
fileFlows: "fileflows",
|
||||
proxmox: "proxmox",
|
||||
nextcloud: "nextcloud",
|
||||
unifiController: "unifi-controller",
|
||||
@@ -60,5 +59,6 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
|
||||
peaNut: "peanut",
|
||||
beszel: "beszel",
|
||||
archiveTeamWarrior: null,
|
||||
dawarich: "dawarich",
|
||||
mock: null,
|
||||
};
|
||||
@@ -51,4 +51,6 @@ export const widgetDocSlugs: Record<WidgetKind, string | null> = {
|
||||
beszelAlerts: "beszel-alerts",
|
||||
beszelSystemStats: "beszel-system-stats",
|
||||
customApi: "custom-api",
|
||||
dawarichStatistics: null,
|
||||
dawarichPlaces: null,
|
||||
};
|
||||
@@ -251,14 +251,6 @@ export const integrationDefs = {
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
|
||||
defaultPort: 8265,
|
||||
},
|
||||
fileFlows: {
|
||||
name: "FileFlows",
|
||||
secretKinds: [[], ["apiKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons@master/png/fileflows.png",
|
||||
category: ["mediaTranscoding"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/fileflows"),
|
||||
defaultPort: 5000,
|
||||
},
|
||||
proxmox: {
|
||||
name: "Proxmox",
|
||||
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
|
||||
@@ -512,6 +504,13 @@ export const integrationDefs = {
|
||||
defaultUrl: "http://localhost:8001",
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/archiveteam-warrior"),
|
||||
},
|
||||
dawarich: {
|
||||
name: "Dawarch",
|
||||
secretKinds: [["apiKey"]],
|
||||
iconUrl: "https://cdn.jsdelivr.net/gh/selfhst/icons/png/dawarich.png",
|
||||
category: ["geolocationTracking"],
|
||||
documentationUrl: createDocumentationLink("/docs/integrations/dawarich"),
|
||||
},
|
||||
// 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",
|
||||
@@ -625,6 +624,7 @@ export const integrationCategories = [
|
||||
"documents",
|
||||
"mediaLibrary",
|
||||
"uptimeMonitoring",
|
||||
"geolocationTracking",
|
||||
] as const;
|
||||
|
||||
export type IntegrationCategory = (typeof integrationCategories)[number];
|
||||
@@ -40,6 +40,8 @@ export const widgetIntegrationSupport: Partial<Record<WidgetKind, readonly Integ
|
||||
audioStats: ["navidrome", "audiobookshelf"],
|
||||
vpn: getIntegrationKindsByCategory("vpn"),
|
||||
anchorNote: ["anchor"],
|
||||
dawarichStatistics: ["dawarich"],
|
||||
dawarichPlaces: ["dawarich"],
|
||||
};
|
||||
|
||||
export const getWidgetKindsForIntegration = (integrationKind: IntegrationKind): WidgetKind[] => {
|
||||
|
||||
@@ -49,6 +49,8 @@ export const widgetKinds = [
|
||||
"beszelAlerts",
|
||||
"beszelSystemStats",
|
||||
"customApi",
|
||||
"dawarichStatistics",
|
||||
"dawarichPlaces",
|
||||
] as const;
|
||||
export type WidgetKind = (typeof widgetKinds)[number];
|
||||
|
||||
|
||||
@@ -33,7 +33,6 @@ 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 { FileFlowsIntegration } from "../media-transcoding/fileflows-integration";
|
||||
import { MockIntegration } from "../mock/mock-integration";
|
||||
import { NavidromeIntegration } from "../navidrome/navidrome-integration";
|
||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||
@@ -61,6 +60,7 @@ import { UptimeKumaIntegration } from "../uptime-kuma/uptime-kuma-integration";
|
||||
import { UnifiControllerIntegration } from "../unifi-controller/unifi-controller-integration";
|
||||
import { UnraidIntegration } from "../unraid/unraid-integration";
|
||||
import type { Integration, IntegrationInput } from "./integration";
|
||||
import { DawarichIntegration } from "../dawarich/dawarich-integration";
|
||||
|
||||
export const createIntegrationAsync = async <TKind extends keyof typeof integrationCreators>(
|
||||
integration: IntegrationInput & { kind: TKind },
|
||||
@@ -110,7 +110,6 @@ export const integrationCreators = {
|
||||
readarr: ReadarrIntegration,
|
||||
dashDot: DashDotIntegration,
|
||||
tdarr: TdarrIntegration,
|
||||
fileFlows: FileFlowsIntegration,
|
||||
proxmox: ProxmoxIntegration,
|
||||
emby: EmbyIntegration,
|
||||
nextcloud: NextcloudIntegration,
|
||||
@@ -145,6 +144,7 @@ export const integrationCreators = {
|
||||
uptimeKuma: UptimeKumaIntegration,
|
||||
peaNut: PeaNutIntegration,
|
||||
beszel: BeszelIntegration,
|
||||
dawarich: DawarichIntegration,
|
||||
} satisfies Record<IntegrationKind, IntegrationInstance | [(input: IntegrationInput) => Promise<Integration>]>;
|
||||
|
||||
type IntegrationInstanceOfKind<TKind extends keyof typeof integrationCreators> = {
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
import { ResponseError } from "@homarr/common/server";
|
||||
import type { IntegrationInput, IntegrationTestingInput } from "../base/integration";
|
||||
import { Integration } from "../base/integration";
|
||||
import { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
|
||||
import type { z } from "zod/v4";
|
||||
import type { TestingResult } from "../base/test-connection/test-connection-service";
|
||||
import { TestConnectionError } from "../base/test-connection/test-connection-error";
|
||||
|
||||
export class DawarichIntegration extends Integration {
|
||||
constructor(integration: IntegrationInput) {
|
||||
super(integration);
|
||||
}
|
||||
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url(`/api/v1/users/me`), {
|
||||
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
|
||||
});
|
||||
|
||||
if (response.status === 200) {
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
return TestConnectionError.StatusResult(response);
|
||||
}
|
||||
|
||||
async getStatisticsAsync(): Promise<{
|
||||
totalDistanceKm: number;
|
||||
totalPointsTracked: number;
|
||||
totalReverseGeocodedPoints: number;
|
||||
totalCountriesVisited: number;
|
||||
totalCitiesVisited: number;
|
||||
}> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/stats"), {
|
||||
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new ResponseError(response);
|
||||
|
||||
return dawarichStatisticsSchema.parseAsync(await response.json());
|
||||
}
|
||||
|
||||
async getPlacesAsync(): Promise<z.infer<typeof dawarichPlacesSchema>> {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/v1/places"), {
|
||||
headers: { Authorization: `Bearer ${this.getSecretValue("apiKey")}` },
|
||||
});
|
||||
|
||||
if (!response.ok) throw new ResponseError(response);
|
||||
|
||||
return dawarichPlacesSchema.parseAsync(await response.json());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const dawarichStatisticsSchema = z.object({
|
||||
totalDistanceKm: z.number(),
|
||||
totalPointsTracked: z.number(),
|
||||
totalReverseGeocodedPoints: z.number(),
|
||||
totalCountriesVisited: z.number(),
|
||||
totalCitiesVisited: z.number(),
|
||||
});
|
||||
|
||||
export const dawarichPlaceTagSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
icon: z.string(),
|
||||
color: z.string(),
|
||||
});
|
||||
|
||||
export const dawarichPlaceSchema = z.object({
|
||||
id: z.number(),
|
||||
name: z.string(),
|
||||
latitude: z.number(),
|
||||
longitude: z.number(),
|
||||
source: z.string().optional(),
|
||||
icon: z.string().nullable(),
|
||||
color: z.string().nullable(),
|
||||
visits_count: z.number(),
|
||||
created_at: z.string(),
|
||||
tags: z.array(dawarichPlaceTagSchema),
|
||||
});
|
||||
|
||||
export const dawarichPlacesSchema = z.array(dawarichPlaceSchema);
|
||||
@@ -0,0 +1,5 @@
|
||||
import type { z } from "zod/v4";
|
||||
import type { dawarichPlacesSchema, dawarichStatisticsSchema } from "./dawarich-schemas";
|
||||
|
||||
export type DawarichStatistics = z.infer<typeof dawarichStatisticsSchema>;
|
||||
export type DawarichPlace = z.infer<typeof dawarichPlacesSchema>[number];
|
||||
@@ -41,6 +41,7 @@ export { NavidromeIntegration } from "./navidrome/navidrome-integration";
|
||||
export { UptimeKumaIntegration } from "./uptime-kuma/uptime-kuma-integration";
|
||||
export { UmamiIntegration } from "./umami/umami-integration";
|
||||
export { PeaNutIntegration } from "./peanut/peanut-integration";
|
||||
export { DawarichIntegration } from "./dawarich/dawarich-integration";
|
||||
|
||||
// Types
|
||||
export type { IntegrationInput } from "./base/integration";
|
||||
@@ -86,6 +87,7 @@ export type { AudiobookshelfDashboardData } from "./audiobookshelf/audiobookshel
|
||||
export type { NavidromeDashboardData } from "./navidrome/navidrome-types";
|
||||
export type { UptimeKumaDashboardData } from "./uptime-kuma/uptime-kuma-types";
|
||||
export type { UmamiVisitorStats } from "./umami/umami-types";
|
||||
export type { DawarichStatistics, DawarichPlace } from "./dawarich/dawarich-types";
|
||||
|
||||
// Schemas
|
||||
export { anchorNotesListInputSchema } from "./anchor/anchor-types";
|
||||
|
||||
@@ -1,200 +0,0 @@
|
||||
import { fetchWithTrustedCertificatesAsync } from "@homarr/core/infrastructure/http";
|
||||
|
||||
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 { IMediaTranscodingIntegration } from "../interfaces/media-transcoding/media-transcoding-integration";
|
||||
import type { TdarrQueue, TdarrStatistics, TdarrWorker } from "../interfaces/media-transcoding/media-transcoding-types";
|
||||
import {
|
||||
libraryStatusSchema,
|
||||
recentlyFinishedSchema,
|
||||
shrinkageGroupsSchema,
|
||||
statusResponseSchema,
|
||||
upcomingFilesSchema,
|
||||
workersResponseSchema,
|
||||
} from "./fileflows-validation-schemas";
|
||||
|
||||
export class FileFlowsIntegration extends Integration implements IMediaTranscodingIntegration {
|
||||
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
|
||||
const response = await input.fetchAsync(this.url("/api/system/version"), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
accept: "application/json",
|
||||
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) return TestConnectionError.StatusResult(response);
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
public async getStatisticsAsync(): Promise<TdarrStatistics> {
|
||||
const headerParams = {
|
||||
accept: "application/json",
|
||||
"Content-Type": "application/json",
|
||||
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
|
||||
};
|
||||
|
||||
const [statusData, libraryStatusData, shrinkageData] = await Promise.all([
|
||||
this.fetchStatusAsync(headerParams),
|
||||
this.fetchLibraryStatusAsync(headerParams),
|
||||
this.fetchShrinkageAsync(headerParams),
|
||||
]);
|
||||
|
||||
const failedCount = libraryStatusData
|
||||
.filter((item) => item.Status === 4)
|
||||
.reduce((sum, item) => sum + item.Count, 0);
|
||||
const processedCount = libraryStatusData
|
||||
.filter((item) => item.Status === 3)
|
||||
.reduce((sum, item) => sum + item.Count, 0);
|
||||
const queuedCount = libraryStatusData
|
||||
.filter((item) => item.Status === 1 || item.Status === 5)
|
||||
.reduce((sum, item) => sum + item.Count, 0);
|
||||
|
||||
const totalSavedSpace = Object.values(shrinkageData).reduce(
|
||||
(sum, group) => sum + Math.max(0, group.OriginalSize - group.FinalSize),
|
||||
0,
|
||||
);
|
||||
|
||||
const transcodeStatus = [
|
||||
{ name: "Processed", value: statusData.processed },
|
||||
{ name: "Queued", value: statusData.queue },
|
||||
{ name: "Processing", value: statusData.processing },
|
||||
{ name: "Failed", value: failedCount },
|
||||
];
|
||||
|
||||
return {
|
||||
libraryName: "All",
|
||||
totalFileCount: processedCount + queuedCount + statusData.processing + failedCount,
|
||||
totalTranscodeCount: statusData.processed,
|
||||
totalHealthCheckCount: 0,
|
||||
failedTranscodeCount: failedCount,
|
||||
failedHealthCheckCount: 0,
|
||||
stagedTranscodeCount: statusData.queue,
|
||||
stagedHealthCheckCount: 0,
|
||||
totalSavedSpace,
|
||||
transcodeStatus,
|
||||
healthCheckStatus: [],
|
||||
videoCodecs: [],
|
||||
videoContainers: [],
|
||||
videoResolutions: [],
|
||||
audioCodecs: [],
|
||||
audioContainers: [],
|
||||
};
|
||||
}
|
||||
|
||||
public async getWorkersAsync(): Promise<TdarrWorker[]> {
|
||||
const headerParams = {
|
||||
"Content-Type": "application/json",
|
||||
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
|
||||
};
|
||||
|
||||
const url = this.url("/api/worker");
|
||||
const response = await fetchWithTrustedCertificatesAsync(url, {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
|
||||
const workersData = await workersResponseSchema.parseAsync(await response.json());
|
||||
|
||||
return workersData.map((worker) => ({
|
||||
id: worker.Uid,
|
||||
filePath: worker.RelativeFile ?? "",
|
||||
fps: 0,
|
||||
percentage: worker.CurrentPartPercent,
|
||||
ETA: worker.ProcessingTime?.TotalSeconds ? `${Math.round(worker.ProcessingTime.TotalSeconds)}s` : "",
|
||||
jobType: worker.CurrentPartName ?? "Processing",
|
||||
status: "Processing",
|
||||
step: worker.CurrentPartName ?? "",
|
||||
originalSize: worker.LibraryFile?.OriginalSize ?? 0,
|
||||
estimatedSize: null,
|
||||
outputSize: null,
|
||||
}));
|
||||
}
|
||||
|
||||
public async getQueueAsync(firstItemIndex: number, pageSize: number): Promise<TdarrQueue> {
|
||||
const headerParams = {
|
||||
"Content-Type": "application/json",
|
||||
...(super.hasSecretValue("apiKey") ? { "X-Api-Key": super.getSecretValue("apiKey") } : {}),
|
||||
};
|
||||
|
||||
const [upcomingItems, recentlyFinishedItems] = await Promise.all([
|
||||
this.fetchUpcomingAsync(headerParams),
|
||||
this.fetchRecentlyFinishedAsync(headerParams),
|
||||
]);
|
||||
|
||||
const queueArray = [
|
||||
...upcomingItems.map((item) => ({
|
||||
id: item.Uid,
|
||||
healthCheck: "",
|
||||
transcode: item.Flow ?? "Default",
|
||||
filePath: item.RelativePath ?? item.Name ?? "",
|
||||
fileSize: item.OriginalSize ?? 0,
|
||||
container: "",
|
||||
codec: "",
|
||||
resolution: "",
|
||||
type: "transcode" as const,
|
||||
})),
|
||||
...recentlyFinishedItems.map((item) => ({
|
||||
id: item.Uid,
|
||||
healthCheck: "",
|
||||
transcode: item.Flow ?? "Default",
|
||||
filePath: item.RelativePath ?? item.Name ?? "",
|
||||
fileSize: item.FinalSize ?? item.OriginalSize ?? 0,
|
||||
container: "",
|
||||
codec: "",
|
||||
resolution: "",
|
||||
type: "health-check" as const,
|
||||
})),
|
||||
].slice(0, pageSize);
|
||||
|
||||
return {
|
||||
array: queueArray,
|
||||
totalCount: upcomingItems.length + recentlyFinishedItems.length,
|
||||
startIndex: firstItemIndex,
|
||||
endIndex: firstItemIndex + queueArray.length - 1,
|
||||
};
|
||||
}
|
||||
|
||||
private async fetchStatusAsync(headerParams: Record<string, string>) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/status"), {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
return statusResponseSchema.parseAsync(await response.json());
|
||||
}
|
||||
|
||||
private async fetchLibraryStatusAsync(headerParams: Record<string, string>) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/library-file/status"), {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
return libraryStatusSchema.parseAsync(await response.json());
|
||||
}
|
||||
|
||||
private async fetchShrinkageAsync(headerParams: Record<string, string>) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/library-file/shrinkage-groups"), {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
return shrinkageGroupsSchema.parseAsync(await response.json());
|
||||
}
|
||||
|
||||
private async fetchUpcomingAsync(headerParams: Record<string, string>) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/library-file/upcoming"), {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
return upcomingFilesSchema.parseAsync(await response.json());
|
||||
}
|
||||
|
||||
private async fetchRecentlyFinishedAsync(headerParams: Record<string, string>) {
|
||||
const response = await fetchWithTrustedCertificatesAsync(this.url("/api/library-file/recently-finished"), {
|
||||
method: "GET",
|
||||
headers: headerParams,
|
||||
});
|
||||
return recentlyFinishedSchema.parseAsync(await response.json());
|
||||
}
|
||||
}
|
||||
@@ -1,98 +0,0 @@
|
||||
import { z } from "zod/v4";
|
||||
|
||||
export const statusResponseSchema = z.object({
|
||||
queue: z.number(),
|
||||
processing: z.number(),
|
||||
processed: z.number(),
|
||||
processingFiles: z
|
||||
.array(
|
||||
z.object({
|
||||
name: z.string().nullable(),
|
||||
relativePath: z.string().nullable(),
|
||||
library: z.string().nullable(),
|
||||
step: z.string().nullable(),
|
||||
stepPercent: z.number(),
|
||||
}),
|
||||
)
|
||||
.optional()
|
||||
.transform((arr) => arr ?? []),
|
||||
});
|
||||
|
||||
export const libraryStatusSchema = z.array(
|
||||
z.object({
|
||||
Name: z.string().nullable(),
|
||||
Status: z.number(),
|
||||
Count: z.number(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const shrinkageGroupsSchema = z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
Library: z.string().nullable(),
|
||||
OriginalSize: z.number(),
|
||||
FinalSize: z.number(),
|
||||
Items: z.number(),
|
||||
Difference: z.number().optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const workersResponseSchema = z.array(
|
||||
z.object({
|
||||
Uid: z.string(),
|
||||
NodeName: z.string().nullable(),
|
||||
RelativeFile: z.string().nullable(),
|
||||
CurrentPart: z.number(),
|
||||
CurrentPartName: z.string().nullable(),
|
||||
CurrentPartPercent: z.number(),
|
||||
StartedAt: z.string(),
|
||||
LastUpdate: z.string(),
|
||||
ProcessingTime: z
|
||||
.object({
|
||||
TotalSeconds: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
LibraryFile: z
|
||||
.object({
|
||||
OriginalSize: z.number(),
|
||||
})
|
||||
.optional(),
|
||||
Library: z
|
||||
.object({
|
||||
Name: z.string().nullable(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const upcomingFilesSchema = z.array(
|
||||
z.object({
|
||||
Uid: z.string(),
|
||||
Name: z.string().nullable(),
|
||||
RelativePath: z.string().nullable(),
|
||||
OriginalSize: z.number().nullable(),
|
||||
FinalSize: z.number().nullable(),
|
||||
Status: z.number(),
|
||||
Library: z.string().nullable(),
|
||||
Flow: z.string().nullable(),
|
||||
Node: z.string().nullable(),
|
||||
ProcessingTime: z
|
||||
.object({
|
||||
TotalSeconds: z.number().optional(),
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
);
|
||||
|
||||
export const recentlyFinishedSchema = z.array(
|
||||
z.object({
|
||||
Uid: z.string(),
|
||||
Name: z.string().nullable(),
|
||||
RelativePath: z.string().nullable(),
|
||||
OriginalSize: z.number().nullable(),
|
||||
FinalSize: z.number().nullable(),
|
||||
Status: z.number(),
|
||||
Library: z.string().nullable(),
|
||||
Flow: z.string().nullable(),
|
||||
}),
|
||||
);
|
||||
@@ -25,3 +25,4 @@ export * from "./paperless-ngx/paperless-ngx-types";
|
||||
export * from "./umami/umami-types";
|
||||
export * from "./gluetun/gluetun-types";
|
||||
export * from "./beszel/beszel-types";
|
||||
export * from "./dawarich/dawarich-types";
|
||||
@@ -0,0 +1,32 @@
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import { createIntegrationAsync } from "@homarr/integrations";
|
||||
import type { DawarichPlace, DawarichStatistics } from "@homarr/integrations";
|
||||
|
||||
import { createCachedIntegrationRequestHandler } from "./lib/cached-integration-request-handler";
|
||||
|
||||
export const dawarichStatisticsRequestHandler = createCachedIntegrationRequestHandler<
|
||||
DawarichStatistics,
|
||||
"dawarich",
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration) {
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
return await instance.getStatisticsAsync();
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
queryKey: "dawarichStatistics",
|
||||
});
|
||||
|
||||
export const dawarichPlacesRequestHandler = createCachedIntegrationRequestHandler<
|
||||
DawarichPlace[],
|
||||
"dawarich",
|
||||
Record<string, never>
|
||||
>({
|
||||
async requestAsync(integration) {
|
||||
const instance = await createIntegrationAsync(integration);
|
||||
return await instance.getPlacesAsync();
|
||||
},
|
||||
cacheDuration: dayjs.duration(5, "minutes"),
|
||||
queryKey: "dawarichPlaces",
|
||||
});
|
||||
@@ -3545,6 +3545,42 @@
|
||||
"executeSuccess": "Action executed successfully",
|
||||
"executeFailed": "Action failed. Check the widget configuration."
|
||||
},
|
||||
"dawarichStatistics": {
|
||||
"name": "Dawarich Statistics",
|
||||
"description": "Display travel statistics from Dawarich",
|
||||
"option": {
|
||||
"showTotalDistance": {
|
||||
"label": "Show total distance"
|
||||
},
|
||||
"showTotalPoints": {
|
||||
"label": "Show total points tracked"
|
||||
},
|
||||
"showReverseGeocoded": {
|
||||
"label": "Show reverse geocoded points"
|
||||
},
|
||||
"showCountries": {
|
||||
"label": "Show countries visited"
|
||||
},
|
||||
"showCities": {
|
||||
"label": "Show cities visited"
|
||||
}
|
||||
},
|
||||
"totalDistance": "Total distance",
|
||||
"totalPoints": "Points tracked",
|
||||
"reverseGeocoded": "Reverse geocoded",
|
||||
"countries": "Countries",
|
||||
"cities": "Cities"
|
||||
},
|
||||
"dawarichPlaces": {
|
||||
"name": "Dawarich Places",
|
||||
"description": "Display recently visited places from Dawarich",
|
||||
"option": {
|
||||
"maxPlaces": {
|
||||
"label": "Max places to show"
|
||||
}
|
||||
},
|
||||
"visits": "visits"
|
||||
},
|
||||
"beszelSystemTable": {
|
||||
"name": "Beszel Systems (Table)",
|
||||
"description": "Table view of all Beszel-monitored systems with sortable columns and status indicators",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
.placeItem {
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--mantine-radius-sm);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
"use client";
|
||||
|
||||
import { Anchor, Group, Stack, Text } from "@mantine/core";
|
||||
import { IconMapPin } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function DawarichPlacesWidget({ integrationIds, options }: WidgetComponentProps<"dawarichPlaces">) {
|
||||
const t = useI18n();
|
||||
const { data: places } = clientApi.widget.dawarich.getPlaces.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
limit: options.maxPlaces,
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
if (!places || places.length === 0) return <WidgetEmptyState />;
|
||||
|
||||
return (
|
||||
<Stack gap="xs" h="100%" p="md">
|
||||
{places.map((place) => (
|
||||
<Group key={place.id} gap="sm" align="center" className={classes.placeItem}>
|
||||
<IconMapPin size={16} />
|
||||
<Text size="sm" fw={500} style={{ flex: 1 }}>
|
||||
{place.name}
|
||||
</Text>
|
||||
<Text size="xs" c="dimmed">
|
||||
{place.visits_count} {t("widget.dawarichPlaces.visits")}
|
||||
</Text>
|
||||
</Group>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { IconMapPin } from "@tabler/icons-react";
|
||||
import z from "zod";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dawarichPlaces", {
|
||||
icon: IconMapPin,
|
||||
supportedIntegrations: ["dawarich"],
|
||||
integrationsRequired: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
maxPlaces: factory.number({ defaultValue: 10, validate: z.number().min(1).max(50) }),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -0,0 +1,10 @@
|
||||
.statItem {
|
||||
padding: 12px;
|
||||
border-radius: var(--mantine-radius-md);
|
||||
background-color: light-dark(var(--mantine-color-gray-0), var(--mantine-color-dark-6));
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background-color: light-dark(var(--mantine-color-gray-1), var(--mantine-color-dark-5));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
"use client";
|
||||
|
||||
import { Group, Stack, Text } from "@mantine/core";
|
||||
import { IconMap, IconMapPin, IconRoute, IconWorld, IconBuildingSkyscraper } from "@tabler/icons-react";
|
||||
|
||||
import { clientApi } from "@homarr/api/client";
|
||||
import { useI18n } from "@homarr/translation/client";
|
||||
|
||||
import { WidgetEmptyState } from "../common/empty-state";
|
||||
import type { WidgetComponentProps } from "../definition";
|
||||
import classes from "./component.module.css";
|
||||
|
||||
export default function DawarichStatisticsWidget({
|
||||
integrationIds,
|
||||
options,
|
||||
}: WidgetComponentProps<"dawarichStatistics">) {
|
||||
const t = useI18n();
|
||||
const { data: stats } = clientApi.widget.dawarich.getStatistics.useQuery(
|
||||
{
|
||||
integrationId: integrationIds[0] ?? "",
|
||||
},
|
||||
{ staleTime: 5 * 60 * 1000 },
|
||||
);
|
||||
|
||||
if (!stats) return <WidgetEmptyState />;
|
||||
|
||||
return (
|
||||
<Stack gap="md" h="100%" p="md">
|
||||
{options.showTotalDistance && (
|
||||
<StatItem
|
||||
icon={<IconRoute size={20} />}
|
||||
label={t("widget.dawarichStatistics.totalDistance")}
|
||||
value={`${stats.totalDistanceKm.toLocaleString(undefined, { maximumFractionDigits: 1 })} km`}
|
||||
/>
|
||||
)}
|
||||
{options.showTotalPoints && (
|
||||
<StatItem
|
||||
icon={<IconMap size={20} />}
|
||||
label={t("widget.dawarichStatistics.totalPoints")}
|
||||
value={stats.totalPointsTracked.toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
{options.showReverseGeocoded && (
|
||||
<StatItem
|
||||
icon={<IconMapPin size={20} />}
|
||||
label={t("widget.dawarichStatistics.reverseGeocoded")}
|
||||
value={stats.totalReverseGeocodedPoints.toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
{options.showCountries && (
|
||||
<StatItem
|
||||
icon={<IconWorld size={20} />}
|
||||
label={t("widget.dawarichStatistics.countries")}
|
||||
value={stats.totalCountriesVisited.toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
{options.showCities && (
|
||||
<StatItem
|
||||
icon={<IconBuildingSkyscraper size={20} />}
|
||||
label={t("widget.dawarichStatistics.cities")}
|
||||
value={stats.totalCitiesVisited.toLocaleString()}
|
||||
/>
|
||||
)}
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
interface StatItemProps {
|
||||
icon: React.ReactNode;
|
||||
label: string;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
function StatItem({ icon, label, value }: StatItemProps) {
|
||||
return (
|
||||
<Group justify="space-between" align="center" className={classes.statItem}>
|
||||
<Group gap="sm" align="center">
|
||||
{icon}
|
||||
<Text size="sm" fw={500}>
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
<Text size="sm" fw={700} c="var(--mantine-primary-color)">
|
||||
{value}
|
||||
</Text>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { IconChartBar } from "@tabler/icons-react";
|
||||
|
||||
import { createWidgetDefinition } from "../definition";
|
||||
import { optionsBuilder } from "../options";
|
||||
|
||||
export const { definition, componentLoader } = createWidgetDefinition("dawarichStatistics", {
|
||||
icon: IconChartBar,
|
||||
supportedIntegrations: ["dawarich"],
|
||||
integrationsRequired: true,
|
||||
createOptions() {
|
||||
return optionsBuilder.from((factory) => ({
|
||||
showTotalDistance: factory.switch({ defaultValue: true }),
|
||||
showTotalPoints: factory.switch({ defaultValue: true }),
|
||||
showReverseGeocoded: factory.switch({ defaultValue: true }),
|
||||
showCountries: factory.switch({ defaultValue: true }),
|
||||
showCities: factory.switch({ defaultValue: true }),
|
||||
}));
|
||||
},
|
||||
}).withDynamicImport(() => import("./component"));
|
||||
@@ -60,6 +60,8 @@ import * as beszelSystemStats from "./beszel-system-stats";
|
||||
import * as video from "./video";
|
||||
import * as weather from "./weather";
|
||||
import * as customApi from "./custom-api";
|
||||
import * as dawarichStatistics from "./dawarich-statistics";
|
||||
import * as dawarichPlaces from "./dawarich-places";
|
||||
|
||||
export type {
|
||||
WidgetDefinition,
|
||||
@@ -121,6 +123,8 @@ export const widgetImports = {
|
||||
beszelAlerts,
|
||||
beszelSystemStats,
|
||||
customApi,
|
||||
dawarichStatistics,
|
||||
dawarichPlaces,
|
||||
} satisfies WidgetImportRecord;
|
||||
|
||||
export type WidgetImports = typeof widgetImports;
|
||||
|
||||
Reference in new issue
Block a user