import path from "path"; import { fetchWithTrustedCertificatesAsync } from "@homarr/certificates/server"; 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 { Aria2Download, Aria2GetClient } from "./aria2-types"; export class Aria2Integration extends DownloadClientIntegration { public async getClientJobsAndStatusAsync(): Promise { const client = this.getClient(); const keys: (keyof Aria2Download)[] = [ "bittorrent", "uploadLength", "uploadSpeed", "downloadSpeed", "totalLength", "completedLength", "files", "status", "gid", ]; const [activeDownloads, waitingDownloads, stoppedDownloads, globalStats] = await Promise.all([ client.tellActive(), client.tellWaiting(0, 1000, keys), client.tellStopped(0, 1000, keys), client.getGlobalStat(), ]); const downloads = [...activeDownloads, ...waitingDownloads, ...stoppedDownloads]; const allPaused = downloads.every((download) => download.status === "paused"); return { status: { types: ["torrent", "miscellaneous"], paused: allPaused, rates: { up: Number(globalStats.uploadSpeed), down: Number(globalStats.downloadSpeed), }, }, items: downloads.map((download, index) => { const totalSize = Number(download.totalLength); const completedSize = Number(download.completedLength); const progress = totalSize > 0 ? completedSize / totalSize : 0; const itemName = download.bittorrent?.info?.name ?? path.basename(download.files[0]?.path ?? "Unknown"); return { index, id: download.gid, name: itemName, type: download.bittorrent ? "torrent" : "miscellaneous", size: totalSize, sent: Number(download.uploadLength), downSpeed: Number(download.downloadSpeed), upSpeed: Number(download.uploadSpeed), time: this.calculateEta(completedSize, totalSize, Number(download.downloadSpeed)), state: this.getState(download.status, Boolean(download.bittorrent)), category: [], progress, }; }), } as DownloadClientJobsAndStatus; } public async pauseQueueAsync(): Promise { const client = this.getClient(); await client.pauseAll(); } public async pauseItemAsync(item: DownloadClientItem): Promise { const client = this.getClient(); await client.pause(item.id); } public async resumeQueueAsync(): Promise { const client = this.getClient(); await client.unpauseAll(); } public async resumeItemAsync(item: DownloadClientItem): Promise { const client = this.getClient(); await client.unpause(item.id); } public async deleteItemAsync(item: DownloadClientItem, fromDisk: boolean): Promise { const client = this.getClient(); // Note: Remove download file is not support by aria2, replace with forceremove if (item.state in ["downloading", "leeching", "paused"]) { await (fromDisk ? client.remove(item.id) : client.forceRemove(item.id)); } else { await client.removeDownloadResult(item.id); } } public async testConnectionAsync(): Promise { const client = this.getClient(); await client.getVersion(); } private getClient() { const url = this.url("/jsonrpc"); return new Proxy( {}, { get: (target, method: keyof Aria2GetClient) => { return async (...args: Parameters) => { let params = [...args]; if (this.hasSecretValue("apiKey")) { params = [`token:${this.getSecretValue("apiKey")}`, ...params]; } const body = JSON.stringify({ jsonrpc: "2.0", id: btoa(["Homarr", Date.now().toString(), Math.random()].join(".")), // unique id per request method: `aria2.${method}`, params, }); return await fetchWithTrustedCertificatesAsync(url, { method: "POST", body }) .then(async (response) => { const responseBody = (await response.json()) as { result: ReturnType }; if (!response.ok) { throw new Error(response.statusText); } return responseBody.result; }) .catch((error) => { if (error instanceof Error) { throw new Error(error.message); } else { throw new Error("Error communicating with Aria2"); } }); }; }, }, ) as Aria2GetClient; } private getState(aria2Status: Aria2Download["status"], isTorrent: boolean): DownloadClientItem["state"] { return isTorrent ? this.getTorrentState(aria2Status) : this.getNonTorrentState(aria2Status); } private getNonTorrentState(aria2Status: Aria2Download["status"]): DownloadClientItem["state"] { switch (aria2Status) { case "active": return "downloading"; case "waiting": return "queued"; case "paused": return "paused"; case "complete": return "completed"; case "error": return "failed"; case "removed": default: return "unknown"; } } private getTorrentState(aria2Status: Aria2Download["status"]): DownloadClientItem["state"] { switch (aria2Status) { case "active": return "leeching"; case "waiting": return "queued"; case "paused": return "paused"; case "complete": return "completed"; case "error": return "failed"; case "removed": default: return "unknown"; } } private calculateEta(completed: number, total: number, speed: number): number { if (speed === 0 || completed >= total) return 0; return Math.floor((total - completed) / speed) * 1000; // Convert to milliseconds } }