Files
mesh-catalog/modules/qbittorrent/index.ts
T
jschoubben 7b06a7a408 Event names are local now, in the manifests and in the code
Every module named its events the way the old bus spelled a routing key —
`module.<module>.<verb>`. Design 29 says a module names an event locally and the
mesh works out where it lands, so all 37 were stale against a rule already
decided. On the new bus that derives into a namespace belonging to a module
called "module", so no cross-module subscription in the mesh matched anything:
nothing failed, nothing reacted (novox/hq 04-ISSUES/127).

36 manifests converted, and 43 files of module code with them. The code mattered
as much as the manifests: the runtime builds the subject from what `emit()` is
handed, so a converted manifest with unconverted code would have had the
permission and the subject disagree.

Three things the new check found on the way:

- `photos` emitted an event its manifest never declared, which the new bus refuses
  outright. Declared.
- `showcase` waited for an event nothing emits, so its demo could never be
  triggered — only `showcase` may publish under its own name. It emits both halves
  now.
- `distribution` declared an event named after a different module. It emits
  `image.pushed` under its own name. An event about a *role* belongs on the seat,
  where the name outlives whoever holds it, but the sdk has no way to publish on a
  seat yet, so that stays recorded rather than declared.

The audit logger's "everything" pattern is `**` rather than the old bus's `#`.
2026-09-27 14:42:28 +02:00

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 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<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("download.added", { name: t.name, category: t.category, sizeBytes: t.sizeBytes });
} else if (before < 1 && t.progress >= 1) {
await emit("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");