nzbget (usenet, JSON-RPC) and qbittorrent (torrents, WebUI API with manual SID session). Tools: status, queue/torrents, add, pause/resume, delete. Both poll and emit module.<app>.download.added and module.<app>.download.completed — the key plex consumes to rescan; completion is keyed off real success (nzbget history SUCCESS, qbittorrent progress reaching 1), not mere queue disappearance, so a failed or deleted item is not reported as done. Typecheck; manifests parse.
53 lines
2.2 KiB
TypeScript
53 lines
2.2 KiB
TypeScript
// qbittorrent's events. The tool runtime imports this once the broker is bound. It watches the
|
|
// torrent list and turns its comings and goings into mesh events.
|
|
//
|
|
// Emits (novox/hq ADR 0046/0047):
|
|
// module.qbittorrent.download.added — a torrent was added
|
|
// module.qbittorrent.download.completed — a torrent finished downloading (progress reached 1).
|
|
// This exact routing key is what the plex module
|
|
// consumes (module.*.download.completed) to rescan, so
|
|
// the new file becomes a visible item.
|
|
// Consumes: none.
|
|
//
|
|
// The torrent list is polled and diffed by hash, primed silently on the first look (like plex's and
|
|
// sonarr's index.ts) so a restart does not re-announce everything already present. Completion is a
|
|
// progress crossing from below 1 to exactly 1 — a torrent added already-complete is announced only
|
|
// as added, never as freshly completed, since nothing was downloaded.
|
|
|
|
import { emit } from "@novox/mesh-sdk/events";
|
|
import { QbittorrentClient } from "./client.js";
|
|
|
|
const qb = QbittorrentClient.fromEnv();
|
|
|
|
const progressByHash = new Map<string, number>();
|
|
let primed = false;
|
|
|
|
async function pollTorrents(): Promise<void> {
|
|
const torrents = await qb.getTorrents();
|
|
const now = new Map(torrents.map((t) => [t.hash, t]));
|
|
|
|
if (primed) {
|
|
for (const [hash, t] of now) {
|
|
const before = progressByHash.get(hash);
|
|
if (before === undefined) {
|
|
await emit("module.qbittorrent.download.added", { name: t.name, category: t.category, sizeBytes: t.sizeBytes });
|
|
} else if (before < 1 && t.progress >= 1) {
|
|
await emit("module.qbittorrent.download.completed", { name: t.name, category: t.category, sizeBytes: t.sizeBytes });
|
|
}
|
|
}
|
|
}
|
|
|
|
progressByHash.clear();
|
|
for (const [hash, t] of now) progressByHash.set(hash, t.progress);
|
|
primed = true;
|
|
}
|
|
|
|
const tick = (fn: () => Promise<void>, everyMs: number): void => {
|
|
const run = (): void => void fn().catch((err) => console.error(`[qbittorrent] ${err}`));
|
|
setInterval(run, everyMs);
|
|
run();
|
|
};
|
|
tick(pollTorrents, 20_000);
|
|
|
|
console.log("[qbittorrent] watching torrents, emitting adds and completions");
|