@torrent-tv/proxy 2.89.15 → 2.89.17

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/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## 2.89.17
2
+
3
+ - **Fix**: Every browser log line the proxy keeps names the page's send it came in, as `batch=<n>`, and the server prints the same number. A send that reached neither side, or a batch the proxy took over a connection that could no longer carry its answer back, is now visible by comparing the two (torrent-tv/meta#77).
4
+
5
+ ## 2.89.16
6
+
7
+ - **Fix**: A proxy started without `--log-file` keeps the browser's log. The page sends its log to the proxy for most of a viewing, and the proxy wrote it only to per-session files beside its own log file; with no log file named, which is every run from npm or Docker other than the Home Assistant addon, the browser's half of every viewing was dropped. It now goes to the console, beside the proxy's lines, each prefixed `client <sessionId>`. `docs/logs.md` states where the log goes for each way of starting the proxy (torrent-tv/meta#96).
8
+
1
9
  ## 2.89.15
2
10
 
3
11
  - **Fix**: A torrent downloaded whole is no longer removed while something is still reading it. The sweep that writes whole files out and removes the torrent asked only who had stated a need, and a read whose pieces are all here waits for nothing and states nothing. On the stand on 2026-10-04 the torrent was removed under ffmpeg's own read of the film: the read ended with "Piece store is closed" 277 MB in, ffmpeg reported `Stream ends prematurely`, the copied picture lost 2.6 s, and the page refused to start with "Prepared media contains a timestamp gap" (torrent-tv/meta#105). Open reads now count as use.
package/README.md CHANGED
@@ -303,6 +303,7 @@ npm start -- --server-url http://localhost:3000
303
303
  | `--name` | hostname | Display name in registry |
304
304
  | `--token` | — | Auth token for register/heartbeat |
305
305
  | `--ffmpeg-bin` | bundled | Path to custom ffmpeg binary |
306
+ | `--log-file` | — | Also write the log to this file, and the browser's lines to `client-*.log` beside it; without it everything goes to the console only. See `docs/logs.md` |
306
307
  | `--no-transcode-audio` | — | Disable HLS audio transcoding |
307
308
  | `--help` | — | Print all options and exit |
308
309
 
@@ -313,6 +314,13 @@ docker build -t torrent-tv-proxy .
313
314
  docker run torrent-tv-proxy --server-url http://my-server:8080
314
315
  ```
315
316
 
317
+ Without `--log-file` the log is only in `docker logs`, which recreating the
318
+ container deletes. To keep it, put it on a volume:
319
+
320
+ ```bash
321
+ docker run -v ttv-proxy-logs:/logs torrent-tv-proxy --server-url http://my-server:8080 --log-file /logs/proxy.log
322
+ ```
323
+
316
324
  ## Full End-to-End Flow
317
325
 
318
326
  ```mermaid
package/docs/logs.md CHANGED
@@ -1,10 +1,37 @@
1
1
  # Logs — where to look
2
2
 
3
3
  No browser console copy-paste is needed: the page forwards its own log. But it
4
- lands in TWO places depending on the phase of the session, and `docker logs` is
5
- only one of them — see "Browser logs are in TWO places" below before concluding
4
+ lands in TWO places depending on the phase of the session — the registry server
5
+ and the proxy — see "Browser logs are in TWO places" below before concluding
6
6
  anything from a half-session.
7
7
 
8
+ ## Where the proxy writes its log, by how it is started
9
+
10
+ The proxy is deployment-agnostic: the Home Assistant addon is one way to run it,
11
+ npm and Docker on Linux, macOS or Windows are others. Where its log goes is
12
+ decided by one start parameter, `--log-file <path>`, and nothing else:
13
+
14
+ - **With `--log-file`**: every line goes to the console AND to that file
15
+ (appended, rotated to `<path>.1` past 1 GiB), and the browser's lines go to
16
+ one file per viewing session in the same directory (see "The files on the
17
+ proxy" below). The directory of the file must exist.
18
+ - **Without it**: everything goes to the console only, the browser's lines
19
+ included, each prefixed `client <sessionId:8>`. Before torrent-tv/meta#96 the
20
+ browser's lines were dropped in this case. The console is kept by whatever
21
+ started the process, and only as long as that keeps it.
22
+
23
+ | How it is started | Log | Browser lines | Survives |
24
+ |---|---|---|---|
25
+ | Home Assistant addon | `/data/proxy.log` (`run.sh` passes `--log-file /data/proxy.log`) | `/data/client-*.log` | restarts and addon updates |
26
+ | npm / `npx`, any OS, no `--log-file` | the terminal | the terminal | nothing |
27
+ | npm under systemd, no `--log-file` | the unit's journal: `journalctl -u <unit>` | the same journal | restarts, within journald's limits |
28
+ | npm with `--log-file`, e.g. `/var/log/torrent-tv-proxy/proxy.log`, `~/Library/Logs/torrent-tv/proxy.log`, `%LOCALAPPDATA%\torrent-tv\proxy.log` | that file | `client-*.log` beside it | restarts and updates |
29
+ | Docker, no `--log-file` | `docker logs <container>` | the same | only the container's life: recreating it deletes the log |
30
+ | Docker with a volume and `--log-file`, e.g. `-v ttv-proxy-logs:/logs … --log-file /logs/proxy.log` | the file in the volume | `client-*.log` in the volume | recreating the container |
31
+
32
+ So on any host where the log matters, pass `--log-file` and point it at a
33
+ directory that outlives the process — which is what the addon does.
34
+
8
35
  ## HA proxy (aarch64, addon `b34a1737_torrent_tv_proxy`)
9
36
 
10
37
  Image `b34a1737/aarch64-addon-torrent_tv_proxy:<version>` = `@torrent-tv/proxy` `<version>` (see `ha-addon/torrent_tv_proxy/config.yaml`).
@@ -30,15 +57,27 @@ SSH to HA requires `MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh
30
57
 
31
58
  ## DO server (webauth.courses, `infra-server-1`)
32
59
 
33
- - Server is `infra-server-1` (`ghcr.io/torrent-tv/server:latest`) on `do` (`206.189.97.152`).
34
- - Frontend logs reach here only in the phases the proxy cannot be reached in — see "Browser logs are in TWO places" below. When they do, the server `console.log`s them with the prefix `[client <tag> <id> sig=<webrtcSessionId>]` (`server/routes/api/client-logs/post.js`), in the same container as its own lines.
35
- - Via container:
60
+ - Server is `infra-server-1` (`ghcr.io/torrent-tv/server`) on `do` (`206.189.97.152`).
61
+ - Every container there logs to the host journal, and rsyslog writes one file per
62
+ source to `/var/log/torrent-tv/`; the files and the journal survive the
63
+ deployments that recreate the containers (`infra` README, "Logs";
64
+ torrent-tv/meta#96):
65
+ - `server.log` — the server's own lines;
66
+ - `client.log` — browser lines the server received, prefixed
67
+ `[client <tag> <id> sig=<webrtcSessionId>]`
68
+ (`server/routes/api/client-logs/post.js`);
69
+ - `nginx.log`, `doco-cd.log`.
70
+ - Frontend logs reach the droplet only in the phases the proxy cannot be reached
71
+ in — see "Browser logs are in TWO places" below.
72
+ - Reading:
36
73
  ```bash
37
- ssh do "docker logs infra-server-1 --tail 200 | cat"
74
+ ssh do "tail -n 300 /var/log/torrent-tv/client.log"
38
75
  # filter a single viewing:
39
- ssh do "docker logs infra-server-1 --tail 500 | grep 003ed2fd"
40
- ssh do "docker logs infra-server-1 --tail 500 | grep '\[client'"
76
+ ssh do "grep 003ed2fd /var/log/torrent-tv/client.log /var/log/torrent-tv/server.log"
77
+ ssh do "journalctl -t torrent-tv-server --since '-1h' -o short-iso-precise"
41
78
  ```
79
+ - `docker logs infra-server-1` shows only the CURRENT container's lines — a
80
+ deployment gives it a new container. Use the files or `journalctl -t`.
42
81
  - No need to open eruda or copy the browser console on a phone — it is forwarded.
43
82
 
44
83
  ## Browser logs are in TWO places, split by phase — read both
@@ -52,13 +91,26 @@ The split is by PHASE, and neither half is the whole session:
52
91
 
53
92
  1. **Before a transport exists** — the page opening, the proxy being chosen, a
54
93
  connection failing — there is nowhere else to send it, so it goes to the
55
- SERVER (`infra-server-1`);
94
+ SERVER (`/var/log/torrent-tv/client.log` on the droplet);
56
95
  2. **From the moment the data channel is up**, the page's logger is given a
57
96
  proxy sink (`loading.js`, `setProxySink`) and every batch goes to the PROXY
58
97
  over that channel. This is the bulk of a viewing;
59
98
  3. **At unload** it goes to the server again: `navigator.sendBeacon` is the only
60
99
  thing a page that is going away can use, and a data channel is not;
61
100
  4. **A batch that cannot reach the proxy** falls back to the server.
101
+ 5. **A batch nobody took** goes back into the page's queue and leaves with the
102
+ next send. What the page lost — lines pushed out by its 500-line queue — and
103
+ every refused send, by route and cause, are written into the log itself as
104
+ one line, `[client-logger] since <time>: …`, which travels with the next
105
+ batch that is delivered.
106
+
107
+ Every line carries the number of the page's send it came in, as `batch=<n>`,
108
+ on both sides: `[Windows/Chrome batch=12]` here, `[client Windows/Chrome
109
+ <sessionId> batch=12]` on the droplet. A number on neither side is a send no
110
+ route took: its lines went out again under a later number, and the report line
111
+ says why. A number on both is a batch the proxy took but whose answer never
112
+ reached the page — a connection wedged in the proxy-to-page direction still
113
+ carries requests the other way — which the page then sent to the server too.
62
114
 
63
115
  So a session that failed while connecting is on the droplet, a session that
64
116
  failed while playing is on the addon host, and a complete reading of a long
@@ -66,8 +118,10 @@ failure needs both.
66
118
 
67
119
  ### The files on the proxy
68
120
 
69
- One file per viewing session, in the proxy log's own directory (`/data` on the
70
- addon host), named so the two halves join without guessing:
121
+ When the proxy was started with `--log-file`: one file per viewing session, in
122
+ the proxy log's own directory (`/data` on the addon host), named so the two
123
+ halves join without guessing. Without `--log-file` the same lines are in the
124
+ proxy's console, prefixed `client <sessionId:8>`.
71
125
 
72
126
  ```
73
127
  client-<YYYYMMDD-HHMMSS UTC of the session start>-<sessionId:8>-<torrent name:60>-<infoHash:8>.log
@@ -86,7 +140,7 @@ Implementation: `utils/client-log-file.js`, route `routes/api/client-logs/post.j
86
140
 
87
141
  ## Quick triage
88
142
 
89
- - Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
143
+ - Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `client.log` `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
90
144
  - Cushion / link budget: HA `memory: rss=... anon=...` and `cushion` lines; DO `[eta]` / `[cushion]` from client.
91
145
  - Update check: `ssh ha "sudo docker exec hassio_cli ha apps info b34a1737_torrent_tv_proxy | grep version"` and `ssh ha "sudo docker ps --filter name=app_b34a1737_torrent_tv_proxy"`.
92
146
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.89.15",
3
+ "version": "2.89.17",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -53,7 +53,13 @@ function sanitizeLine(value) {
53
53
  * `POST /api/client-logs`
54
54
  *
55
55
  * Body: `{ sessionId, startedAt, torrentName, infoHash, tag, signalSessionId,
56
- * lines: [{ level, ts, msg }] }`.
56
+ * seq, lines: [{ level, ts, msg }] }`.
57
+ *
58
+ * `seq` numbers the page's sends and is printed as `batch=<seq>`. The server
59
+ * prints the same number, so a batch that reached neither side shows as a gap,
60
+ * and one taken here whose answer never reached the page — a wedged connection
61
+ * still carries the request — shows on both, because the page then sends it to
62
+ * the server too.
57
63
  *
58
64
  * @param {import("fastify").FastifyRequest} req
59
65
  * @param {import("fastify").FastifyReply} reply
@@ -69,6 +75,7 @@ export async function handleApiClientLogsPost(req, reply, { clientLogs }) {
69
75
  infoHash: safeString(body.infoHash, 40)
70
76
  };
71
77
  const tag = sanitizeLine(safeString(body.tag, MAX_TAG_LEN)) || "Unknown/Unknown";
78
+ const batch = Number.isSafeInteger(body.seq) && body.seq > 0 ? ` batch=${body.seq}` : "";
72
79
  const rows = Array.isArray(body.lines) ? body.lines.slice(0, MAX_LINES) : [];
73
80
 
74
81
  const lines = rows.map((row) => {
@@ -76,7 +83,7 @@ export async function handleApiClientLogsPost(req, reply, { clientLogs }) {
76
83
  const level = safeString(entry.level, 8) || "log";
77
84
  const at = sanitizeLine(safeString(entry.ts, 16));
78
85
  const message = sanitizeLine(safeString(entry.msg, MAX_MSG_LEN));
79
- return `[${tag}] ${at} ${level}: ${message}`;
86
+ return `[${tag}${batch}] ${at} ${level}: ${message}`;
80
87
  });
81
88
 
82
89
  if (lines.length > 0) {
package/server.js CHANGED
@@ -25,7 +25,7 @@ import { handleApiSourceWarmPost } from "./routes/api/sources/warm/post.js";
25
25
  import { handleApiPlaybackPlanPost } from "./routes/api/playback-plan/post.js";
26
26
  import { handleApiPlaybackPlanAudioTracksPost } from "./routes/api/playback-plan/audio-tracks/post.js";
27
27
  import { handleApiClientLogsPost } from "./routes/api/client-logs/post.js";
28
- import { createClientLogFiles } from "./utils/client-log-file.js";
28
+ import { createClientLogConsole, createClientLogFiles } from "./utils/client-log-file.js";
29
29
  import { handleApiSubtitlesGet } from "./routes/api/subtitles/get.js";
30
30
  import { handleApiTranscodeSessionsPost } from "./routes/api/transcode-sessions/post.js";
31
31
  import { handleApiTranscodeSessionsProgressGet } from "./routes/api/transcode-sessions/progress/get.js";
@@ -629,9 +629,12 @@ export async function startProxyServer({
629
629
  }
630
630
  })
631
631
  );
632
- // The browser's own log, kept beside the proxy's. See the route's own file
633
- // for why it is here and not only on the registry server.
634
- const clientLogs = logFile ? createClientLogFiles(logFile) : null;
632
+ // The browser's own log, kept beside the proxy's: a file per session when
633
+ // the proxy writes its log to a file, its console otherwise. See the route's
634
+ // own file for why it is here and not only on the registry server.
635
+ const clientLogs = logFile
636
+ ? createClientLogFiles(logFile)
637
+ : createClientLogConsole((message) => logger.info(message));
635
638
  app.post("/api/client-logs", async (req, reply) =>
636
639
  handleApiClientLogsPost(req, reply, { clientLogs })
637
640
  );
@@ -12,7 +12,7 @@ import { mkdtempSync, readdirSync, readFileSync } from "node:fs";
12
12
  import { tmpdir } from "node:os";
13
13
  import { join } from "node:path";
14
14
 
15
- import { createClientLogFiles } from "../utils/client-log-file.js";
15
+ import { createClientLogConsole, createClientLogFiles } from "../utils/client-log-file.js";
16
16
 
17
17
  /** A directory with a proxy log in it, which is what the sink is given. */
18
18
  function aDirectory() {
@@ -108,3 +108,19 @@ test("no proxy log path means nothing is written and nothing throws", async () =
108
108
  logs.write(aSession(), ["dropped"]);
109
109
  await logs.close();
110
110
  });
111
+
112
+ test("without a log file the lines go to the console, each naming its session", async () => {
113
+ // A proxy started from npm or Docker names no file; the browser's half of
114
+ // the viewing must still reach the output the host keeps.
115
+ const printed = [];
116
+ const logs = createClientLogConsole((message) => printed.push(message));
117
+ logs.write(aSession(), ["first line", "second line"]);
118
+ logs.write(aSession({ sessionId: "" }), ["no id"]);
119
+ await logs.close();
120
+
121
+ assert.deepEqual(printed, [
122
+ "client abcd1234 first line",
123
+ "client abcd1234 second line",
124
+ "client unknown no id"
125
+ ]);
126
+ });
@@ -0,0 +1,34 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import { handleApiClientLogsPost } from "../routes/api/client-logs/post.js";
5
+
6
+ /**
7
+ * Post one body and return the lines the route wrote.
8
+ *
9
+ * @param {object} body
10
+ * @returns {Promise<string[]>}
11
+ */
12
+ async function linesWrittenFor(body) {
13
+ /** @type {string[]} */
14
+ const written = [];
15
+ const reply = { code: () => reply, send: () => reply };
16
+ await handleApiClientLogsPost({ body }, reply, {
17
+ clientLogs: { write: (_session, lines) => written.push(...lines) }
18
+ });
19
+ return written;
20
+ }
21
+
22
+ const line = { level: "info", ts: "12:00:00.000", msg: "hello" };
23
+
24
+ test("each line names the page's send it came in, so the two receivers can be compared", async () => {
25
+ assert.deepEqual(await linesWrittenFor({ tag: "Windows/Chrome", seq: 12, lines: [line] }), [
26
+ "[Windows/Chrome batch=12] 12:00:00.000 info: hello"
27
+ ]);
28
+ });
29
+
30
+ test("a page that numbers nothing is written as before", async () => {
31
+ assert.deepEqual(await linesWrittenFor({ tag: "Windows/Chrome", seq: "12", lines: [line] }), [
32
+ "[Windows/Chrome] 12:00:00.000 info: hello"
33
+ ]);
34
+ });
@@ -16,6 +16,32 @@
16
16
  import { createWriteStream, mkdirSync, renameSync, statSync } from "node:fs";
17
17
  import { dirname, join } from "node:path";
18
18
 
19
+ /**
20
+ * Where a browser's lines go when the proxy was started without `--log-file`.
21
+ *
22
+ * The proxy runs as the Home Assistant addon, from npm, in Docker, on Linux,
23
+ * macOS or Windows; only the addon names a file. Without one these lines were
24
+ * dropped, so on every other host the browser's half of a viewing existed
25
+ * nowhere at all. They go to the proxy's own console instead, beside the
26
+ * proxy's lines, and reach whatever keeps that output on the host: `docker
27
+ * logs`, the journal of a systemd unit, a terminal. Each line carries the
28
+ * session id, which is what the file name carries when there is a file.
29
+ *
30
+ * @param {(message: string) => void} log - The proxy's logger.
31
+ * @returns {{ write: (session: object, lines: string[]) => void, close: () => Promise<void> }}
32
+ */
33
+ export function createClientLogConsole(log) {
34
+ return {
35
+ write(session, lines) {
36
+ const id = safeName(session?.sessionId, 8) || "unknown";
37
+ for (const line of lines) {
38
+ log(`client ${id} ${line}`);
39
+ }
40
+ },
41
+ async close() {}
42
+ };
43
+ }
44
+
19
45
  /** One file may reach this before it is rotated. */
20
46
  const MAX_BYTES = 16 * 1024 * 1024;
21
47