Compare commits

...
Author SHA1 Message Date
manuel-rw 83738bfebf style: auto-fix lint and format 2026-07-01 10:23:39 +00:00
Manuel 5089a2ae6b feat: add fileflows integration 2026-07-01 12:22:12 +02:00
9 changed files with 355 additions and 0 deletions

No files matched your search

@@ -0,0 +1,36 @@
---
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: []
}]} />
@@ -0,0 +1,8 @@
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",
};
@@ -36,6 +36,7 @@ const integrationAliases: Partial<Record<IntegrationKind, readonly string[]>> =
linuxServerIO: ["linuxserver"],
coolify: ["coolify"],
truenas: ["truenas"],
fileFlows: ["fileflows"],
};
export const matchIntegrationKind = (search: string): IntegrationKind | null => {
@@ -312,6 +312,7 @@ 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"
@@ -27,6 +27,7 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
dashDot: "dash-dot",
glances: "glances",
tdarr: "tdarr",
fileFlows: "fileflows",
proxmox: "proxmox",
nextcloud: "nextcloud",
unifiController: "unifi-controller",
+8
View File
@@ -251,6 +251,14 @@ 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"]],
@@ -33,6 +33,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 { FileFlowsIntegration } from "../media-transcoding/fileflows-integration";
import { MockIntegration } from "../mock/mock-integration";
import { NavidromeIntegration } from "../navidrome/navidrome-integration";
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
@@ -109,6 +110,7 @@ export const integrationCreators = {
readarr: ReadarrIntegration,
dashDot: DashDotIntegration,
tdarr: TdarrIntegration,
fileFlows: FileFlowsIntegration,
proxmox: ProxmoxIntegration,
emby: EmbyIntegration,
nextcloud: NextcloudIntegration,
@@ -0,0 +1,200 @@
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());
}
}
@@ -0,0 +1,98 @@
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(),
}),
);