diff --git a/src/server/api/routers/tdarr.ts b/src/server/api/routers/tdarr.ts index bdc2fece388..657108584ab 100644 --- a/src/server/api/routers/tdarr.ts +++ b/src/server/api/routers/tdarr.ts @@ -6,17 +6,7 @@ import { getConfig } from '~/tools/config/getConfig'; import { ConfigAppType } from '~/types/app'; import { createTRPCRouter, publicProcedure } from '../trpc'; - -const inputSchema = z.object({ - appId: z.string(), - configName: z.string(), -}); - -const inputSchemeQueue = inputSchema.extend({ - showHealthChecksInQueue: z.boolean(), - pageSize: z.number(), - page: z.number(), -}); +import { TdarrQueue, TdarrStatistics, TdarrWorker } from '~/types/api/tdarr'; const getStatisticsSchema = z.object({ totalFileCount: z.number(), @@ -87,36 +77,6 @@ const getStatisticsSchema = z.object({ ), }); -export type TdarrPieSegment = { - name: string; - value: number; -}; - -export type TdarrStatistics = { - totalFileCount: number; - totalTranscodeCount: number; - totalHealthCheckCount: number; - failedTranscodeCount: number; - failedHealthCheckCount: number; - stagedTranscodeCount: number; - stagedHealthCheckCount: number; - pies: { - libraryName: string; - libraryId: string; - totalFiles: number; - totalTranscodes: number; - savedSpace: number; - totalHealthChecks: number; - transcodeStatus: TdarrPieSegment[]; - healthCheckStatus: TdarrPieSegment[]; - videoCodecs: TdarrPieSegment[]; - videoContainers: TdarrPieSegment[]; - videoResolutions: TdarrPieSegment[]; - audioCodecs: TdarrPieSegment[]; - audioContainers: TdarrPieSegment[]; - }[]; -}; - const getNodesResponseSchema = z.record( z.string(), z.object({ @@ -148,20 +108,6 @@ const getNodesResponseSchema = z.record( }) ); -export type TdarrWorker = { - id: string; - file: string; - fps: number; - percentage: number; - ETA: string; - jobType: string; - status: string; - step: string; - originalSize: number; - estimatedSize: number | null; - outputSize: number | null; -}; - const getStatusTableSchema = z.object({ array: z.array( z.object({ @@ -178,26 +124,12 @@ const getStatusTableSchema = z.object({ totalCount: z.number(), }); -export type TdarrQueue = { - array: { - id: string; - healthCheck: string; - transcode: string; - file: string; - fileSize: number; - container: string; - codec: string; - resolution: string; - type: 'transcode' | 'health check'; - }[]; - totalCount: number; - startIndex: number; - endIndex: number; -}; - export const tdarrRouter = createTRPCRouter({ statistics: publicProcedure - .input(inputSchema) + .input(z.object({ + appId: z.string(), + configName: z.string(), + })) .query(async ({ input }): Promise => { const app = getTdarrApp(input.appId, input.configName); const appUrl = new URL('api/v2/cruddb', app.url); @@ -239,117 +171,129 @@ export const tdarrRouter = createTRPCRouter({ }; }), - workers: publicProcedure.input(inputSchema).query(async ({ input }): Promise => { - const app = getTdarrApp(input.appId, input.configName); - const appUrl = new URL('api/v2/get-nodes', app.url); + workers: publicProcedure + .input(z.object({ + appId: z.string(), + configName: z.string(), + })).query(async ({ input }): Promise => { + const app = getTdarrApp(input.appId, input.configName); + const appUrl = new URL('api/v2/get-nodes', app.url); + + const res = await axios.get(appUrl.toString()); + const data = getNodesResponseSchema.parse(res.data); + + const nodes = Object.values(data); + const workers = nodes.flatMap((node) => { + return Object.values(node.workers); + }); + + return workers.map((worker) => ({ + id: worker._id, + filePath: worker.file, + fps: worker.fps, + percentage: worker.percentage, + ETA: worker.ETA, + jobType: worker.job.type, + status: worker.status, + step: worker.lastPluginDetails?.number ?? '', + originalSize: worker.originalfileSizeInGbytes * 1_000_000_000, // file_size is in GB, convert to bytes, + estimatedSize: worker.estSize ? worker.estSize * 1_000_000_000 : null, // file_size is in GB, convert to bytes, + outputSize: worker.outputFileSizeInGbytes ? worker.outputFileSizeInGbytes * 1_000_000_000 : null, // file_size is in GB, convert to bytes, + })); + }), - const res = await axios.get(appUrl.toString()); - const data = getNodesResponseSchema.parse(res.data); + queue: publicProcedure + .input(z.object({ + appId: z.string(), + configName: z.string(), + showHealthChecksInQueue: z.boolean(), + pageSize: z.number(), + page: z.number(), + })) + .query(async ({ input }): Promise => { + const app = getTdarrApp(input.appId, input.configName); - const nodes = Object.values(data); - const workers = nodes.flatMap((node) => { - return Object.values(node.workers); - }); + const appUrl = new URL('api/v2/client/status-tables', app.url); - return workers.map((worker) => ({ - id: worker._id, - file: worker.file, - fps: worker.fps, - percentage: worker.percentage, - ETA: worker.ETA, - jobType: worker.job.type, - status: worker.status, - step: worker.lastPluginDetails?.number ?? '', - originalSize: worker.originalfileSizeInGbytes * 1_000_000, // file_size is in MB, convert to bytes, - estimatedSize: worker.estSize ? worker.estSize * 1_000_000 : null, // file_size is in MB, convert to bytes, - outputSize: worker.outputFileSizeInGbytes ? worker.outputFileSizeInGbytes * 1_000_000 : null, // file_size is in MB, convert to bytes, - })); - }), + const { page, pageSize, showHealthChecksInQueue } = input; - queue: publicProcedure.input(inputSchemeQueue).query(async ({ input }): Promise => { - const app = getTdarrApp(input.appId, input.configName); + const firstItemIndex = page * pageSize; - const appUrl = new URL('api/v2/client/status-tables', app.url); + const transcodeQueueBody = { + data: { + start: firstItemIndex, + pageSize: pageSize, + filters: [], + sorts: [], + opts: { + table: 'table1', + }, + }, + }; - const { page, pageSize, showHealthChecksInQueue } = input; + const transcodeQueueRes = await axios.post(appUrl.toString(), transcodeQueueBody); + const transcodeQueueData = getStatusTableSchema.parse(transcodeQueueRes.data); + + const transcodeQueueResult = { + array: transcodeQueueData.array.map((item) => ({ + id: item._id, + healthCheck: item.HealthCheck, + transcode: item.TranscodeDecisionMaker, + filePath: item.file, + fileSize: item.file_size * 1_000_000, // file_size is in MB, convert to bytes + container: item.container, + codec: item.video_codec_name, + resolution: item.video_resolution, + type: 'transcode' as const, + })), + totalCount: transcodeQueueData.totalCount, + startIndex: firstItemIndex, + endIndex: firstItemIndex + transcodeQueueData.array.length - 1, + }; - const firstItemIndex = page * pageSize; + if (!showHealthChecksInQueue) { + return transcodeQueueResult; + } - const transcodeQueueBody = { - data: { - start: firstItemIndex, - pageSize: pageSize, - filters: [], - sorts: [], - opts: { - table: 'table1', + const healthCheckQueueBody = { + data: { + start: Math.max(firstItemIndex - transcodeQueueData.totalCount, 0), + pageSize: pageSize, + filters: [], + sorts: [], + opts: { + table: 'table4', + }, }, - }, - }; + }; - const transcodeQueueRes = await axios.post(appUrl.toString(), transcodeQueueBody); - const transcodeQueueData = getStatusTableSchema.parse(transcodeQueueRes.data); + const healthCheckQueueRes = await axios.post(appUrl.toString(), healthCheckQueueBody); + const healthCheckQueueData = getStatusTableSchema.parse(healthCheckQueueRes.data); - const transcodeQueueResult = { - array: transcodeQueueData.array.map((item) => ({ + const healthCheckResultArray = healthCheckQueueData.array.map((item) => ({ id: item._id, healthCheck: item.HealthCheck, transcode: item.TranscodeDecisionMaker, - file: item.file, + filePath: item.file, fileSize: item.file_size * 1_000_000, // file_size is in MB, convert to bytes container: item.container, codec: item.video_codec_name, resolution: item.video_resolution, - type: 'transcode' as const, - })), - totalCount: transcodeQueueData.totalCount, - startIndex: firstItemIndex, - endIndex: firstItemIndex + transcodeQueueData.array.length - 1, - }; - - if (!showHealthChecksInQueue) { - return transcodeQueueResult; - } + type: 'health check' as const, + })); - const healthCheckQueueBody = { - data: { - start: Math.max(firstItemIndex - transcodeQueueData.totalCount, 0), - pageSize: pageSize, - filters: [], - sorts: [], - opts: { - table: 'table4', - }, - }, - }; + const combinedArray = [...transcodeQueueResult.array, ...healthCheckResultArray].slice( + 0, + pageSize + ); - const healthCheckQueueRes = await axios.post(appUrl.toString(), healthCheckQueueBody); - const healthCheckQueueData = getStatusTableSchema.parse(healthCheckQueueRes.data); - - const healthCheckResultArray = healthCheckQueueData.array.map((item) => ({ - id: item._id, - healthCheck: item.HealthCheck, - transcode: item.TranscodeDecisionMaker, - file: item.file, - fileSize: item.file_size * 1_000_000, // file_size is in MB, convert to bytes - container: item.container, - codec: item.video_codec_name, - resolution: item.video_resolution, - type: 'health check' as const, - })); - - const combinedArray = [...transcodeQueueResult.array, ...healthCheckResultArray].slice( - 0, - pageSize - ); - - return { - array: combinedArray, - totalCount: transcodeQueueData.totalCount + healthCheckQueueData.totalCount, - startIndex: firstItemIndex, - endIndex: firstItemIndex + combinedArray.length - 1, - }; - }), + return { + array: combinedArray, + totalCount: transcodeQueueData.totalCount + healthCheckQueueData.totalCount, + startIndex: firstItemIndex, + endIndex: firstItemIndex + combinedArray.length - 1, + }; + }), }); function getTdarrApp(appId: string, configName: string): ConfigAppType { diff --git a/src/types/api/tdarr.ts b/src/types/api/tdarr.ts new file mode 100644 index 00000000000..751611b40d8 --- /dev/null +++ b/src/types/api/tdarr.ts @@ -0,0 +1,60 @@ +export type TdarrPieSegment = { + name: string; + value: number; +}; + +export type TdarrStatistics = { + totalFileCount: number; + totalTranscodeCount: number; + totalHealthCheckCount: number; + failedTranscodeCount: number; + failedHealthCheckCount: number; + stagedTranscodeCount: number; + stagedHealthCheckCount: number; + pies: { + libraryName: string; + libraryId: string; + totalFiles: number; + totalTranscodes: number; + savedSpace: number; + totalHealthChecks: number; + transcodeStatus: TdarrPieSegment[]; + healthCheckStatus: TdarrPieSegment[]; + videoCodecs: TdarrPieSegment[]; + videoContainers: TdarrPieSegment[]; + videoResolutions: TdarrPieSegment[]; + audioCodecs: TdarrPieSegment[]; + audioContainers: TdarrPieSegment[]; + }[]; +}; + +export type TdarrWorker = { + id: string; + filePath: string; + fps: number; + percentage: number; + ETA: string; + jobType: string; + status: string; + step: string; + originalSize: number; + estimatedSize: number | null; + outputSize: number | null; +}; + +export type TdarrQueue = { + array: { + id: string; + healthCheck: string; + transcode: string; + filePath: string; + fileSize: number; + container: string; + codec: string; + resolution: string; + type: 'transcode' | 'health check'; + }[]; + totalCount: number; + startIndex: number; + endIndex: number; +}; \ No newline at end of file diff --git a/src/widgets/tdarr/Filename.tsx b/src/widgets/tdarr/Filename.tsx index 8129eeec677..f73944c867f 100644 --- a/src/widgets/tdarr/Filename.tsx +++ b/src/widgets/tdarr/Filename.tsx @@ -1,11 +1,11 @@ import { Text } from '@mantine/core'; interface FilenameProps { - filename: string; + filePath: string; } export function Filename(props: FilenameProps) { - const { filename } = props; + const { filePath } = props; return ( - {filename.substring(filename.lastIndexOf('/') + 1)} + {filePath.split('\\').pop()?.split('/').pop() ?? filePath} ); } diff --git a/src/widgets/tdarr/HealthCheckStatus.tsx b/src/widgets/tdarr/HealthCheckStatus.tsx index 957afdc58e1..d3fb9534827 100644 --- a/src/widgets/tdarr/HealthCheckStatus.tsx +++ b/src/widgets/tdarr/HealthCheckStatus.tsx @@ -11,7 +11,8 @@ import { import { IconHeartbeat } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { useColorScheme } from '~/hooks/use-colorscheme'; -import { TdarrStatistics } from '~/server/api/routers/tdarr'; + +import { TdarrStatistics } from '~/types/api/tdarr'; interface StatisticsBadgeProps { statistics?: TdarrStatistics; @@ -40,7 +41,7 @@ export function HealthCheckStatus(props: StatisticsBadgeProps) { - + diff --git a/src/widgets/tdarr/QueuePanel.tsx b/src/widgets/tdarr/QueuePanel.tsx index a51c1429868..1b7f483834c 100644 --- a/src/widgets/tdarr/QueuePanel.tsx +++ b/src/widgets/tdarr/QueuePanel.tsx @@ -1,10 +1,10 @@ import { Center, Group, ScrollArea, Table, Text, Title, Tooltip } from '@mantine/core'; import { IconHeartbeat, IconTransform } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { TdarrQueue } from '~/server/api/routers/tdarr'; import { humanFileSize } from '~/tools/humanFileSize'; import { WidgetLoading } from '~/widgets/loading'; import { Filename } from '~/widgets/tdarr/Filename'; +import { TdarrQueue } from '~/types/api/tdarr'; interface QueuePanelProps { queue: TdarrQueue | undefined; @@ -55,7 +55,7 @@ export function QueuePanel(props: QueuePanelProps) { )} - + diff --git a/src/widgets/tdarr/StatisticsPanel.tsx b/src/widgets/tdarr/StatisticsPanel.tsx index 3191d6cd3c1..6fd09a03810 100644 --- a/src/widgets/tdarr/StatisticsPanel.tsx +++ b/src/widgets/tdarr/StatisticsPanel.tsx @@ -18,9 +18,9 @@ import { } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; import { ReactNode } from 'react'; -import { TdarrPieSegment, TdarrStatistics } from '~/server/api/routers/tdarr'; import { humanFileSize } from '~/tools/humanFileSize'; import { WidgetLoading } from '~/widgets/loading'; +import { TdarrPieSegment, TdarrStatistics } from '~/types/api/tdarr'; const PIE_COLORS: MantineColor[] = ['cyan', 'grape', 'gray', 'orange', 'pink']; @@ -51,7 +51,7 @@ export function StatisticsPanel(props: StatisticsPanelProps) { } return ( - + app.id === widget.properties.appId); + const fallbackAppId = config?.apps.find( + (app) => app.integration.type === 'tdarr', + )?.id; + const app = config?.apps.find((app) => app.id === widget.properties.appId || fallbackAppId); const { defaultView, showHealthCheck, showHealthChecksInQueue, queuePageSize, showAppIcon } = widget.properties; @@ -100,7 +103,7 @@ function TdarrQueueTile({ widget }: TdarrQueueTileProps) { viewSchema.parse(defaultView) ); - const [page, setPage] = useState(1); + const [queuePage, setQueuePage] = useState(1); const workers = api.tdarr.workers.useQuery( { @@ -123,7 +126,7 @@ function TdarrQueueTile({ widget }: TdarrQueueTileProps) { appId: app?.id!, configName: configName!, pageSize: queuePageSize, - page: page - 1, + page: queuePage - 1, showHealthChecksInQueue, }, { @@ -230,12 +233,12 @@ function TdarrQueueTile({ widget }: TdarrQueueTileProps) { /> {view === 'queue' && !!queue.data && ( <> - + - - - - + + + + diff --git a/src/widgets/tdarr/WorkersPanel.tsx b/src/widgets/tdarr/WorkersPanel.tsx index b5ac2cea84e..7ebaa79cc70 100644 --- a/src/widgets/tdarr/WorkersPanel.tsx +++ b/src/widgets/tdarr/WorkersPanel.tsx @@ -10,9 +10,9 @@ import { } from '@mantine/core'; import { IconHeartbeat, IconTransform } from '@tabler/icons-react'; import { useTranslation } from 'next-i18next'; -import { TdarrWorker } from '~/server/api/routers/tdarr'; import { WidgetLoading } from '~/widgets/loading'; import { Filename } from '~/widgets/tdarr/Filename'; +import { TdarrWorker } from '~/types/api/tdarr'; interface WorkersPanelProps { workers: TdarrWorker[] | undefined; @@ -64,7 +64,7 @@ export function WorkersPanel(props: WorkersPanelProps) { )} - +