Files
homarr/packages/integrations/src/download-client/nzbget/nzbget-integration.ts

157 lines
5.8 KiB
TypeScript

import dayjs from "dayjs";
import type { fetch as undiciFetch } from "undici";
import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server";
import { ResponseError } from "@homarr/common/server";
import type { IntegrationTestingInput } from "../../base/integration";
import type { TestingResult } from "../../base/test-connection/test-connection-service";
import type { DownloadClientJobsAndStatus } from "../../interfaces/downloads/download-client-data";
import { DownloadClientIntegration } from "../../interfaces/downloads/download-client-integration";
import type { DownloadClientItem } from "../../interfaces/downloads/download-client-items";
import type { DownloadClientStatus } from "../../interfaces/downloads/download-client-status";
import type { NzbGetClient } from "./nzbget-types";
export class NzbGetIntegration extends DownloadClientIntegration {
protected async testingAsync(input: IntegrationTestingInput): Promise<TestingResult> {
await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version");
return {
success: true,
};
}
public async getClientJobsAndStatusAsync(input: { limit: number }): Promise<DownloadClientJobsAndStatus> {
const type = "usenet";
const queue = await this.nzbGetApiCallAsync("listgroups");
const history = await this.nzbGetApiCallAsync("history");
const nzbGetStatus = await this.nzbGetApiCallAsync("status");
const status: DownloadClientStatus = {
paused: nzbGetStatus.DownloadPaused,
rates: { down: nzbGetStatus.DownloadRate },
types: [type],
};
const items = queue
.map((file): DownloadClientItem => {
const state = NzbGetIntegration.getUsenetQueueState(file.Status);
const time =
(file.RemainingSizeLo + file.RemainingSizeHi * Math.pow(2, 32)) / (nzbGetStatus.DownloadRate / 1000);
return {
type,
id: file.NZBID.toString(),
index: file.MaxPriority,
name: file.NZBName,
size: file.FileSizeLo + file.FileSizeHi * Math.pow(2, 32),
downSpeed: file.ActiveDownloads > 0 ? nzbGetStatus.DownloadRate : 0,
time: Number.isFinite(time) ? time : 0,
added: (dayjs().unix() - file.DownloadTimeSec) * 1000,
state,
progress: file.DownloadedSizeMB / file.FileSizeMB,
category: file.Category,
};
})
.concat(
history.map((file, index): DownloadClientItem => {
const state = NzbGetIntegration.getUsenetHistoryState(file.ScriptStatus);
return {
type,
id: file.NZBID.toString(),
index,
name: file.Name,
size: file.FileSizeLo + file.FileSizeHi * Math.pow(2, 32),
time: (dayjs().unix() - file.HistoryTime) * 1000,
added: (file.HistoryTime - file.DownloadTimeSec) * 1000,
state,
progress: 1,
category: file.Category,
};
}),
)
.slice(0, input.limit);
return { status, items };
}
public async pauseQueueAsync() {
await this.nzbGetApiCallAsync("pausedownload");
}
public async pauseItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.nzbGetApiCallAsync("editqueue", "GroupPause", "", [Number(id)]);
}
public async resumeQueueAsync() {
await this.nzbGetApiCallAsync("resumedownload");
}
public async resumeItemAsync({ id }: DownloadClientItem): Promise<void> {
await this.nzbGetApiCallAsync("editqueue", "GroupResume", "", [Number(id)]);
}
public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise<void> {
if (fromDisk) {
const filesIds = (await this.nzbGetApiCallAsync("listfiles", 0, 0, Number(id))).map((file) => file.ID);
await this.nzbGetApiCallAsync("editqueue", "FileDelete", "", filesIds);
}
if (progress === 1) {
await this.nzbGetApiCallAsync("editqueue", "GroupFinalDelete", "", [Number(id)]);
} else {
await this.nzbGetApiCallAsync("editqueue", "HistoryFinalDelete", "", [Number(id)]);
}
}
private async nzbGetApiCallAsync<CallType extends keyof NzbGetClient>(
method: CallType,
...params: Parameters<NzbGetClient[CallType]>
): Promise<ReturnType<NzbGetClient[CallType]>> {
return await this.nzbGetApiCallWithCustomFetchAsync(fetchWithTrustedCertificatesAsync, method, ...params);
}
private async nzbGetApiCallWithCustomFetchAsync<CallType extends keyof NzbGetClient>(
fetchAsync: typeof undiciFetch,
method: CallType,
...params: Parameters<NzbGetClient[CallType]>
): Promise<ReturnType<NzbGetClient[CallType]>> {
const username = this.getSecretValue("username");
const password = this.getSecretValue("password");
const url = this.url(`/${encodeURIComponent(username)}:${encodeURIComponent(password)}/jsonrpc`);
const body = JSON.stringify({ method, params });
return await fetchAsync(url, { method: "POST", body })
.then(async (response) => {
if (!response.ok) {
throw new ResponseError(response);
}
return ((await response.json()) as { result: ReturnType<NzbGetClient[CallType]> }).result;
})
.catch((error) => {
if (error instanceof Error) {
throw error;
}
throw new Error("Error communicating with NzbGet", {
cause: error,
});
});
}
private static getUsenetQueueState(status: string): DownloadClientItem["state"] {
switch (status) {
case "QUEUED":
return "queued";
case "PAUSED":
return "paused";
default:
return "downloading";
}
}
private static getUsenetHistoryState(status: string): DownloadClientItem["state"] {
switch (status) {
case "FAILURE":
return "failed";
case "SUCCESS":
return "completed";
default:
return "processing";
}
}
}