🏗️ Migrate usenet info to tRPC

This commit is contained in:
Meier Lukas
2023-06-10 17:20:06 +02:00
parent e2352100f8
commit 8c676c9e16
7 changed files with 289 additions and 12 deletions

View File

@@ -13,25 +13,27 @@ import { UsenetInfoRequestParams, UsenetInfoResponse } from '../../../pages/api/
import { UsenetPauseRequestParams } from '../../../pages/api/modules/usenet/pause';
import { queryClient } from '../../../tools/server/configurations/tanstack/queryClient.tool';
import { UsenetResumeRequestParams } from '../../../pages/api/modules/usenet/resume';
import { api } from '~/utils/api';
import { useConfigContext } from '~/config/provider';
const POLLING_INTERVAL = 2000;
export const useGetUsenetInfo = (params: UsenetInfoRequestParams) =>
useQuery(
['usenetInfo', params.appId],
async () =>
(
await axios.get<UsenetInfoResponse>('/api/modules/usenet', {
params,
})
).data,
export const useGetUsenetInfo = ({ appId }: UsenetInfoRequestParams) => {
const { name: configName } = useConfigContext();
return api.usenet.info.useQuery(
{
appId,
configName: configName!,
},
{
refetchInterval: POLLING_INTERVAL,
keepPreviousData: true,
retry: 2,
enabled: Boolean(params.appId),
enabled: !!appId,
}
);
};
export const useGetUsenetDownloads = (params: UsenetQueueRequestParams) =>
useQuery(

View File

@@ -10,6 +10,7 @@ import { downloadRouter } from './routers/download';
import { mediaRequestsRouter } from './routers/media-request';
import { mediaServerRouter } from './routers/media-server';
import { overseerrRouter } from './routers/overseerr';
import { usenetRouter } from './routers/usenet/route';
/**
* This is the primary router for your server.
@@ -28,6 +29,7 @@ export const rootRouter = createTRPCRouter({
mediaRequest: mediaRequestsRouter,
mediaServer: mediaServerRouter,
overseerr: overseerrRouter,
usenet: usenetRouter,
});
// export type definition of API

View File

@@ -6,8 +6,8 @@ import Consola from 'consola';
import dayjs from 'dayjs';
import { Client } from 'sabnzbd-api';
import { z } from 'zod';
import { NzbgetClient } from '~/pages/api/modules/usenet/nzbget/nzbget-client';
import { NzbgetQueueItem, NzbgetStatus } from '~/pages/api/modules/usenet/nzbget/types';
import { NzbgetClient } from '~/server/api/routers/usenet/nzbget/nzbget-client';
import { NzbgetQueueItem, NzbgetStatus } from '~/server/api/routers/usenet/nzbget/types';
import { getConfig } from '~/tools/config/getConfig';
import {
NormalizedDownloadAppStat,

View File

@@ -0,0 +1 @@
declare module 'nzbget-api';

View File

@@ -0,0 +1,22 @@
import NZBGet from 'nzbget-api';
import { NzbgetClientOptions } from './types';
export function NzbgetClient(options: NzbgetClientOptions) {
if (!options?.host) {
throw new Error('Cannot connect to NZBGet. Missing host in app config.');
}
if (!options?.port) {
throw new Error('Cannot connect to NZBGet. Missing port in app config.');
}
if (!options?.login) {
throw new Error('Cannot connect to NZBGet. Missing username in app config.');
}
if (!options?.hash) {
throw new Error('Cannot connect to NZBGet. Missing password in app config.');
}
return new NZBGet(options);
}

View File

@@ -0,0 +1,149 @@
export interface NzbgetHistoryItem {
NZBID: number;
Kind: 'NZB' | 'URL' | 'DUP';
NZBFilename: string;
Name: string;
URL: string;
HistoryTime: number;
DestDir: string;
FinalDir: string;
Category: string;
FileSizeLo: number;
FileSizeHi: number;
FileSizeMB: number;
FileCount: number;
RemainingFileCount: number;
MinPostTime: number;
MaxPostTime: number;
TotalArticles: number;
SuccessArticles: number;
FailedArticles: number;
Health: number;
DownloadedSizeLo: number;
DownloadedSizeHi: number;
DownloadedSizeMB: number;
DownloadTimeSec: number;
PostTotalTimeSec: number;
ParTimeSec: number;
RepairTimeSec: number;
UnpackTimeSec: number;
MessageCount: number;
DupeKey: string;
DupeScore: number;
DupeMode: 'SCORE' | 'ALL' | 'FORCE';
Status: string;
ParStatus: 'NONE' | 'FAILURE' | 'REPAIR_POSSIBLE' | 'SUCCESS' | 'MANUAL';
ExParStatus: 'RECIPIENT' | 'DONOR';
UnpackStatus: 'NONE' | 'FAILURE' | 'SPACE' | 'PASSWORD' | 'SUCCESS';
UrlStatus: 'NONE' | 'SUCCESS' | 'FAILURE' | 'SCAN_SKIPPED' | 'SCAN_FAILURE';
ScriptStatus: 'NONE' | 'FAILURE' | 'SUCCESS';
ScriptStatuses: [];
MoveStatus: 'NONE' | 'SUCCESS' | 'FAILURE';
DeleteStatus: 'NONE' | 'MANUAL' | 'HEALTH' | 'DUPE' | 'BAD' | 'SCAN' | 'COPY';
MarkStatus: 'NONE' | 'GOOD' | 'BAD';
ExtraParBlocks: number;
Parameters: [];
ServerStats: [];
}
export interface NzbgetQueueItem {
NZBID: number;
NZBFilename: string;
NZBName: string;
Kind: 'NZB' | 'URL';
URL: string;
DestDir: string;
FinalDir: string;
Category: string;
FileSizeLo: number;
FileSizeHi: number;
FileSizeMB: number;
RemainingSizeLo: number;
RemainingSizeHi: number;
RemainingSizeMB: number;
PausedSizeLo: number;
PausedSizeHi: number;
PausedSizeMB: number;
FileCount: number;
RemainingFileCount: number;
RemainingParCount: number;
MinPostTime: number;
MaxPostTime: number;
MaxPriority: number;
ActiveDownloads: number;
Status:
| 'QUEUED'
| 'PAUSED'
| 'DOWNLOADING'
| 'FETCHING'
| 'PP_QUEUED'
| 'LOADING_PARS'
| 'VERIFYING_SOURCES'
| 'REPAIRING'
| 'VERIFYING_REPAIRED'
| 'RENAMING'
| 'UNPACKING'
| 'MOVING'
| 'EXECUTING_SCRIPT'
| 'PP_FINISHED';
TotalArticles: number;
SuccessArticles: number;
FailedArticles: number;
Health: number;
CriticalHealth: number;
DownloadedSizeLo: number;
DownloadedSizeHi: number;
DownloadedSizeMB: number;
DownloadTimeSec: number;
MessageCount: number;
DupeKey: string;
DupeScore: number;
DupeMode: string;
Parameters: [];
ServerStats: [];
PostInfoText: string;
PostStageProgress: number;
PostTotalTimeSec: number;
PostStageTimeSec: number;
}
export interface NzbgetStatus {
RemainingSizeLo: number;
RemainingSizeHi: number;
RemainingSizeMB: number;
ForcedSizeLo: number;
ForcedSizeHi: number;
ForcedSizeMB: number;
DownloadedSizeLo: number;
DownloadedSizeHi: number;
DownloadedSizeMB: number;
ArticleCacheLo: number;
ArticleCacheHi: number;
ArticleCacheMB: number;
DownloadRate: number;
AverageDownloadRate: number;
DownloadLimit: number;
ThreadCount: number;
PostJobCount: number;
UrlCount: number;
UpTimeSec: number;
DownloadTimeSec: number;
ServerStandBy: boolean;
DownloadPaused: boolean;
PostPaused: boolean;
ScanPaused: boolean;
ServerTime: number;
ResumeTime: number;
FeedActive: boolean;
FreeDiskSpaceLo: number;
FreeDiskSpaceHi: number;
FreeDiskSpaceMB: number;
NewsServers: [];
}
export interface NzbgetClientOptions {
host: string;
port: string;
login: string | undefined;
hash: string | undefined;
}

View File

@@ -0,0 +1,101 @@
import dayjs from 'dayjs';
import { Client } from 'sabnzbd-api';
import { z } from 'zod';
import { TRPCError } from '@trpc/server';
import { NzbgetStatus } from '~/server/api/routers/usenet/nzbget/types';
import { getConfig } from '~/tools/config/getConfig';
import { createTRPCRouter, publicProcedure } from '../../trpc';
import { NzbgetClient } from './nzbget/nzbget-client';
export const usenetRouter = createTRPCRouter({
info: publicProcedure
.input(
z.object({
configName: z.string(),
appId: z.string(),
})
)
.query(async ({ input }) => {
const config = getConfig(input.configName);
const app = config.apps.find((x) => x.id === input.appId);
if (!app || (app.integration?.type !== 'nzbGet' && app.integration?.type !== 'sabnzbd')) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `App with ID "${input.appId}" could not be found.`,
});
}
if (app.integration?.type === 'nzbGet') {
const url = new URL(app.url);
const options = {
host: url.hostname,
port: url.port || (url.protocol === 'https:' ? '443' : '80'),
login: app.integration.properties.find((x) => x.field === 'username')?.value ?? undefined,
hash: app.integration.properties.find((x) => x.field === 'password')?.value ?? undefined,
};
const nzbGet = NzbgetClient(options);
const nzbgetStatus: NzbgetStatus = await new Promise((resolve, reject) => {
nzbGet.status((err: any, result: NzbgetStatus) => {
if (!err) {
resolve(result);
} else {
reject(err);
}
});
});
if (!nzbgetStatus) {
throw new TRPCError({
code: 'INTERNAL_SERVER_ERROR',
message: 'Error while getting NZBGet status',
});
}
const bytesRemaining = nzbgetStatus.RemainingSizeMB * 1000000;
const eta = bytesRemaining / nzbgetStatus.DownloadRate;
return {
paused: nzbgetStatus.DownloadPaused,
sizeLeft: bytesRemaining,
speed: nzbgetStatus.DownloadRate,
eta,
};
}
const apiKey = app.integration.properties.find((x) => x.field === 'apiKey')?.value;
if (!apiKey) {
throw new TRPCError({
code: 'BAD_REQUEST',
message: `API Key for app "${app.name}" is missing`,
});
}
const { origin } = new URL(app.url);
const queue = await new Client(origin, apiKey).queue(0, -1);
const [hours, minutes, seconds] = queue.timeleft.split(':');
const eta = dayjs.duration({
hour: parseInt(hours, 10),
minutes: parseInt(minutes, 10),
seconds: parseInt(seconds, 10),
} as any);
return {
paused: queue.paused,
sizeLeft: parseFloat(queue.mbleft) * 1024 * 1024,
speed: parseFloat(queue.kbpersec) * 1000,
eta: eta.asSeconds(),
};
}),
});
export interface UsenetInfoResponse {
paused: boolean;
sizeLeft: number;
speed: number;
eta: number;
}