Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
83738bfebf | ||
|
|
5089a2ae6b |
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"],
|
linuxServerIO: ["linuxserver"],
|
||||||
coolify: ["coolify"],
|
coolify: ["coolify"],
|
||||||
truenas: ["truenas"],
|
truenas: ["truenas"],
|
||||||
|
fileFlows: ["fileflows"],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const matchIntegrationKind = (search: string): IntegrationKind | null => {
|
export const matchIntegrationKind = (search: string): IntegrationKind | null => {
|
||||||
|
|||||||
@@ -312,6 +312,7 @@ export type HomarrDocumentationPath =
|
|||||||
| "/docs/integrations/dash-dot"
|
| "/docs/integrations/dash-dot"
|
||||||
| "/docs/integrations/glances"
|
| "/docs/integrations/glances"
|
||||||
| "/docs/integrations/tdarr"
|
| "/docs/integrations/tdarr"
|
||||||
|
| "/docs/integrations/fileflows"
|
||||||
| "/docs/integrations/proxmox"
|
| "/docs/integrations/proxmox"
|
||||||
| "/docs/integrations/nextcloud"
|
| "/docs/integrations/nextcloud"
|
||||||
| "/docs/integrations/unifi-controller"
|
| "/docs/integrations/unifi-controller"
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ export const integrationDocSlugs: Record<IntegrationKind, string | null> = {
|
|||||||
dashDot: "dash-dot",
|
dashDot: "dash-dot",
|
||||||
glances: "glances",
|
glances: "glances",
|
||||||
tdarr: "tdarr",
|
tdarr: "tdarr",
|
||||||
|
fileFlows: "fileflows",
|
||||||
proxmox: "proxmox",
|
proxmox: "proxmox",
|
||||||
nextcloud: "nextcloud",
|
nextcloud: "nextcloud",
|
||||||
unifiController: "unifi-controller",
|
unifiController: "unifi-controller",
|
||||||
|
|||||||
@@ -251,6 +251,14 @@ export const integrationDefs = {
|
|||||||
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
|
documentationUrl: createDocumentationLink("/docs/integrations/tdarr"),
|
||||||
defaultPort: 8265,
|
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: {
|
proxmox: {
|
||||||
name: "Proxmox",
|
name: "Proxmox",
|
||||||
secretKinds: [["username", "tokenId", "apiKey", "realm"]],
|
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 { ReadarrIntegration } from "../media-organizer/readarr/readarr-integration";
|
||||||
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
import { SonarrIntegration } from "../media-organizer/sonarr/sonarr-integration";
|
||||||
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
import { TdarrIntegration } from "../media-transcoding/tdarr-integration";
|
||||||
|
import { FileFlowsIntegration } from "../media-transcoding/fileflows-integration";
|
||||||
import { MockIntegration } from "../mock/mock-integration";
|
import { MockIntegration } from "../mock/mock-integration";
|
||||||
import { NavidromeIntegration } from "../navidrome/navidrome-integration";
|
import { NavidromeIntegration } from "../navidrome/navidrome-integration";
|
||||||
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
import { NextcloudIntegration } from "../nextcloud/nextcloud.integration";
|
||||||
@@ -109,6 +110,7 @@ export const integrationCreators = {
|
|||||||
readarr: ReadarrIntegration,
|
readarr: ReadarrIntegration,
|
||||||
dashDot: DashDotIntegration,
|
dashDot: DashDotIntegration,
|
||||||
tdarr: TdarrIntegration,
|
tdarr: TdarrIntegration,
|
||||||
|
fileFlows: FileFlowsIntegration,
|
||||||
proxmox: ProxmoxIntegration,
|
proxmox: ProxmoxIntegration,
|
||||||
emby: EmbyIntegration,
|
emby: EmbyIntegration,
|
||||||
nextcloud: NextcloudIntegration,
|
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(),
|
||||||
|
}),
|
||||||
|
);
|
||||||
Reference in new issue
Block a user