@torrent-tv/proxy 2.9.24 → 2.9.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/opsx/apply.md +155 -0
- package/.claude/commands/opsx/archive.md +160 -0
- package/.claude/commands/opsx/explore.md +174 -0
- package/.claude/commands/opsx/propose.md +109 -0
- package/.claude/commands/opsx/sync.md +143 -0
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -0
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -0
- package/.claude/skills/openspec-explore/SKILL.md +289 -0
- package/.claude/skills/openspec-propose/SKILL.md +113 -0
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -0
- package/CHANGELOG.md +5 -0
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -0
- package/openspec/changes/proxy-observability/design.md +38 -0
- package/openspec/changes/proxy-observability/proposal.md +49 -0
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -0
- package/openspec/changes/proxy-observability/tasks.md +20 -0
- package/openspec/config.yaml +38 -0
- package/package.json +1 -1
- package/routes/health/get.js +3 -2
- package/routes/healthz/get.js +3 -2
- package/server.js +5 -2
- package/services/port-mapper.js +7 -0
- package/services/torrent-pool.js +47 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Tasks: Proxy observability
|
|
2
|
+
|
|
3
|
+
## 1. Implementation
|
|
4
|
+
|
|
5
|
+
- [x] 1.1 Version in `/healthz` and `/health` (createRequire package.json in
|
|
6
|
+
server.js, passed as route dep)
|
|
7
|
+
- [x] 1.2 torrent-pool.js: added-torrent line (files/private/trackers),
|
|
8
|
+
torrent `warning` logging, tracker `update` (announce seeders/leechers)
|
|
9
|
+
logging with defensive access, client-level warning logging
|
|
10
|
+
- [x] 1.3 port-mapper.js: setMaxListeners(0) on the UPnP SSDP emitter after
|
|
11
|
+
the first successful map()
|
|
12
|
+
- [x] 1.4 Syntax checks + healthz handler smoke test (version present)
|
|
13
|
+
|
|
14
|
+
## 2. Release
|
|
15
|
+
|
|
16
|
+
- [x] 2.1 CHANGELOG.md entry at 2.9.25
|
|
17
|
+
- [ ] 2.2 `npm run patch` (needs npm auth), then ha-addon bump 0.2.47 + push
|
|
18
|
+
- [ ] 2.3 After the addon updates: verify version via `/healthz`, watch the
|
|
19
|
+
addon log for announce lines on a real torrent, confirm no SSDP
|
|
20
|
+
warnings
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
schema: spec-driven
|
|
2
|
+
|
|
3
|
+
context: |
|
|
4
|
+
@torrent-tv/proxy — WebTorrent + ffmpeg, published to npm. Downloads a torrent
|
|
5
|
+
and streams the chosen file to the browser over a WebRTC data channel (plain
|
|
6
|
+
HTTPS transport planned), transcoding to HLS only for the track(s) the browser
|
|
7
|
+
cannot play natively (supported track = copied).
|
|
8
|
+
Constraints (see CLAUDE.md for the full set):
|
|
9
|
+
- Deployment-agnostic: the HA addon is only one way to run it. No
|
|
10
|
+
Home-Assistant assumptions; runtime detection with graceful fallback only.
|
|
11
|
+
- Routes: routes/<path>/<method>.js exporting handle<Name><Method>.
|
|
12
|
+
- All code, comments and docs in English.
|
|
13
|
+
- Every behavioural change adds a CHANGELOG.md entry at current package.json
|
|
14
|
+
version + 1 patch. Never edit package.json version (npm run patch bumps it).
|
|
15
|
+
- Any behavioural proxy change requires bumping ha-addon config.yaml version
|
|
16
|
+
in the same changeset (the addon must be re-released to pull the new proxy).
|
|
17
|
+
- HLS is VOD-only with server-side seeking (never -hls_playlist_type event).
|
|
18
|
+
- Hardware encoders are gated by a strict startup test; software libx264 is
|
|
19
|
+
the fallback.
|
|
20
|
+
|
|
21
|
+
# Project context (optional)
|
|
22
|
+
# This is shown to AI when creating artifacts.
|
|
23
|
+
# Add your tech stack, conventions, style guides, domain knowledge, etc.
|
|
24
|
+
# Example:
|
|
25
|
+
# context: |
|
|
26
|
+
# Tech stack: TypeScript, React, Node.js
|
|
27
|
+
# We use conventional commits
|
|
28
|
+
# Domain: e-commerce platform
|
|
29
|
+
|
|
30
|
+
# Per-artifact rules (optional)
|
|
31
|
+
# Add custom rules for specific artifacts.
|
|
32
|
+
# Example:
|
|
33
|
+
# rules:
|
|
34
|
+
# proposal:
|
|
35
|
+
# - Keep proposals under 500 words
|
|
36
|
+
# - Always include a "Non-goals" section
|
|
37
|
+
# tasks:
|
|
38
|
+
# - Break tasks into chunks of max 2 hours
|
package/package.json
CHANGED
package/routes/health/get.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @param {import("fastify").FastifyRequest} _req
|
|
7
7
|
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ version: string }} deps
|
|
8
9
|
* @returns {Promise<void>}
|
|
9
10
|
*/
|
|
10
|
-
export async function handleHealthGet(_req, reply) {
|
|
11
|
-
return reply.send({ ok: true });
|
|
11
|
+
export async function handleHealthGet(_req, reply, { version } = {}) {
|
|
12
|
+
return reply.send({ ok: true, version });
|
|
12
13
|
}
|
package/routes/healthz/get.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
*
|
|
6
6
|
* @param {import("fastify").FastifyRequest} _req
|
|
7
7
|
* @param {import("fastify").FastifyReply} reply
|
|
8
|
+
* @param {{ version: string }} deps
|
|
8
9
|
* @returns {Promise<void>}
|
|
9
10
|
*/
|
|
10
|
-
export async function handleHealthzGet(_req, reply) {
|
|
11
|
-
return reply.send({ ok: true });
|
|
11
|
+
export async function handleHealthzGet(_req, reply, { version } = {}) {
|
|
12
|
+
return reply.send({ ok: true, version });
|
|
12
13
|
}
|
package/server.js
CHANGED
|
@@ -12,6 +12,7 @@ import fastifyHelmet from "@fastify/helmet";
|
|
|
12
12
|
import fastifyStatic from "@fastify/static";
|
|
13
13
|
import getPort from "get-port";
|
|
14
14
|
import path from "node:path";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
15
16
|
import { fileURLToPath } from "node:url";
|
|
16
17
|
import { handleHealthGet } from "./routes/health/get.js";
|
|
17
18
|
import { handleHealthzGet } from "./routes/healthz/get.js";
|
|
@@ -32,6 +33,8 @@ import { logger } from "./utils/logger.js";
|
|
|
32
33
|
|
|
33
34
|
const __filename = fileURLToPath(import.meta.url);
|
|
34
35
|
const __dirname = path.dirname(__filename);
|
|
36
|
+
const require = createRequire(import.meta.url);
|
|
37
|
+
const { version } = require("./package.json");
|
|
35
38
|
const publicRoot = path.resolve(__dirname, "./public");
|
|
36
39
|
|
|
37
40
|
/**
|
|
@@ -121,8 +124,8 @@ export async function startProxyServer({ host, port, transcodeAudio, ffmpegBin }
|
|
|
121
124
|
torrentPool
|
|
122
125
|
});
|
|
123
126
|
|
|
124
|
-
app.get("/health", async (req, reply) => handleHealthGet(req, reply));
|
|
125
|
-
app.get("/healthz", async (req, reply) => handleHealthzGet(req, reply));
|
|
127
|
+
app.get("/health", async (req, reply) => handleHealthGet(req, reply, { version }));
|
|
128
|
+
app.get("/healthz", async (req, reply) => handleHealthzGet(req, reply, { version }));
|
|
126
129
|
app.post("/api/sources", async (req, reply) =>
|
|
127
130
|
handleApiSourcesPost(req, reply, { sourceRegistry })
|
|
128
131
|
);
|
package/services/port-mapper.js
CHANGED
|
@@ -157,6 +157,13 @@ export function createPortMapper({
|
|
|
157
157
|
`map ${p}`
|
|
158
158
|
);
|
|
159
159
|
mappedCount++;
|
|
160
|
+
if (mappedCount === 1) {
|
|
161
|
+
// The UPnP SSDP emitter gains one listener per map()/renewal; a
|
|
162
|
+
// 10-port range exceeds Node's default limit of 10 and floods the
|
|
163
|
+
// log with MaxListenersExceededWarning. The client is created
|
|
164
|
+
// lazily by the first map(), so lift the limit right after it.
|
|
165
|
+
instance._upnpClient?.ssdp?.setMaxListeners?.(0);
|
|
166
|
+
}
|
|
160
167
|
} catch (error) {
|
|
161
168
|
logger.warn(`port-mapper: failed to map ${protocol} ${p}: ${describeError(error)}`);
|
|
162
169
|
}
|
package/services/torrent-pool.js
CHANGED
|
@@ -110,6 +110,50 @@ export class TorrentPool {
|
|
|
110
110
|
this.client.on("error", (error) => {
|
|
111
111
|
logger.error(`WebTorrent client error: ${error.message}`);
|
|
112
112
|
});
|
|
113
|
+
this.client.on("warning", (warning) => {
|
|
114
|
+
const message = warning instanceof Error ? warning.message : String(warning);
|
|
115
|
+
logger.warn(`torrent-pool: client warning: ${message}`);
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Attach peer-discovery diagnostics to a freshly added torrent: tracker
|
|
121
|
+
* announce results (seeders/leechers per announce) and torrent-level
|
|
122
|
+
* warnings (tracker rejections/errors surface here). Without these a
|
|
123
|
+
* zero-peer torrent gives no clue WHY it has no peers.
|
|
124
|
+
*
|
|
125
|
+
* @param {string} label - Short source label for log lines.
|
|
126
|
+
* @param {import("webtorrent").Torrent} torrent
|
|
127
|
+
* @returns {void}
|
|
128
|
+
*/
|
|
129
|
+
#attachSwarmDiagnostics(label, torrent) {
|
|
130
|
+
const trackerCount = Array.isArray(torrent.announce) ? torrent.announce.length : 0;
|
|
131
|
+
logger.info(
|
|
132
|
+
`torrent-pool: [${label}] added: files=${torrent.files?.length ?? 0} ` +
|
|
133
|
+
`private=${torrent.private ? "yes" : "no"} trackers=${trackerCount}`
|
|
134
|
+
);
|
|
135
|
+
|
|
136
|
+
torrent.on("warning", (warning) => {
|
|
137
|
+
const message = warning instanceof Error ? warning.message : String(warning);
|
|
138
|
+
logger.warn(`torrent-pool: [${label}] warning: ${message}`);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// bittorrent-tracker's Client emits "update" with each announce response.
|
|
142
|
+
// `complete`/`incomplete` are the tracker's seeder/leecher counts — the
|
|
143
|
+
// authoritative answer to "does the tracker accept us and does the swarm
|
|
144
|
+
// have anyone in it". Internal API, so strictly best-effort.
|
|
145
|
+
const tracker = torrent.discovery?.tracker;
|
|
146
|
+
if (tracker && typeof tracker.on === "function") {
|
|
147
|
+
tracker.on("update", (data) => {
|
|
148
|
+
const announceUrl = typeof data?.announce === "string" ? data.announce : "?";
|
|
149
|
+
logger.info(
|
|
150
|
+
`torrent-pool: [${label}] announce ${announceUrl}: ` +
|
|
151
|
+
`seeders=${data?.complete ?? "?"} leechers=${data?.incomplete ?? "?"}`
|
|
152
|
+
);
|
|
153
|
+
});
|
|
154
|
+
} else {
|
|
155
|
+
logger.info(`torrent-pool: [${label}] tracker client not exposed; announce results not logged`);
|
|
156
|
+
}
|
|
113
157
|
}
|
|
114
158
|
|
|
115
159
|
/**
|
|
@@ -148,6 +192,9 @@ export class TorrentPool {
|
|
|
148
192
|
this.client.off("error", onError);
|
|
149
193
|
this.torrents.set(key, readyTorrent);
|
|
150
194
|
this.#pending.delete(key);
|
|
195
|
+
// Key layout is `${sourceType}:${sha1}`; log with the sha1 prefix so
|
|
196
|
+
// lines correlate with the [stats] source key.
|
|
197
|
+
this.#attachSwarmDiagnostics(key.split(":")[1]?.slice(0, 8) ?? key, readyTorrent);
|
|
151
198
|
resolve(readyTorrent);
|
|
152
199
|
});
|
|
153
200
|
});
|