// plex's events. The tool runtime imports this once the broker is bound, and it does two things: // it watches the server and emits what happened, and it reacts to the mesh's media events. // // Emits (novox/hq ADR 0041/0042): // module.plex.playback.started / .stopped — someone began or ended watching // module.plex.item.added — a new item appeared in a library // Consumes: // module.*.download.completed — a downloader finished; rescan so the file shows up // // The polling is deliberately unhurried: Plex is a neighbour on the same node, and an event a few // seconds late is an event, whereas hammering the server for immediacy nobody asked for is not. import { emit, on } from "@novox/mesh-sdk/events"; import { PlexClient, type PlexSession } from "./client.js"; const plex = PlexClient.fromEnv(); // Playback, by diffing the set of active sessions. Primed silently on the first look so a server // that was already streaming when this started does not announce it as freshly begun. const active = new Map(); let playbackPrimed = false; async function pollSessions(): Promise { const sessions = await plex.getSessions(); const now = new Map(sessions.map((s) => [s.key, s])); if (playbackPrimed) { for (const [key, s] of now) { if (!active.has(key)) await emit("playback.started", { title: s.title, user: s.user, player: s.player, kind: s.type }); } for (const [key, s] of active) { if (!now.has(key)) await emit("playback.stopped", { title: s.title, user: s.user, player: s.player }); } } active.clear(); for (const [key, s] of now) active.set(key, s); playbackPrimed = true; } // New items, by diffing recently-added. Primed silently too, or a restart would re-announce the // whole recent list as new. const seen = new Set(); let itemsPrimed = false; async function pollRecent(): Promise { const items = await plex.getRecentlyAdded(20); for (const item of items) { const id = `${item.title}@${item.addedAt ?? ""}`; if (!seen.has(id)) { if (itemsPrimed) await emit("item.added", item); seen.add(id); } } itemsPrimed = true; } // A downloader finished somewhere on the mesh: rescan, so what it fetched becomes a visible item // rather than a file Plex has not noticed. Idempotent — a rescan too many costs a little disk I/O. await on("*.download.completed", async () => { await plex.refreshAll(); }); const tick = (fn: () => Promise, everyMs: number): void => { const run = (): void => void fn().catch((err) => console.error(`[plex] ${err}`)); setInterval(run, everyMs); run(); }; tick(pollSessions, 15_000); tick(pollRecent, 60_000); console.log("[plex] watching sessions and recently-added, reacting to downloads");