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 { await this.nzbGetApiCallWithCustomFetchAsync(input.fetchAsync, "version"); return { success: true, }; } public async getClientJobsAndStatusAsync(input: { limit: number }): Promise { 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 { await this.nzbGetApiCallAsync("editqueue", "GroupPause", "", [Number(id)]); } public async resumeQueueAsync() { await this.nzbGetApiCallAsync("resumedownload"); } public async resumeItemAsync({ id }: DownloadClientItem): Promise { await this.nzbGetApiCallAsync("editqueue", "GroupResume", "", [Number(id)]); } public async deleteItemAsync({ id, progress }: DownloadClientItem, fromDisk: boolean): Promise { 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( method: CallType, ...params: Parameters ): Promise> { return await this.nzbGetApiCallWithCustomFetchAsync(fetchWithTrustedCertificatesAsync, method, ...params); } private async nzbGetApiCallWithCustomFetchAsync( fetchAsync: typeof undiciFetch, method: CallType, ...params: Parameters ): Promise> { 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 }).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"; } } }