// 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 0041/0042): // 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(); let primed = false; async function pollTorrents(): Promise { 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, 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");