@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 +8 -0
- package/README.md +8 -0
- package/docs/logs.md +66 -12
- package/package.json +1 -1
- package/routes/api/client-logs/post.js +9 -2
- package/server.js +7 -4
- package/test/client-log-file.test.js +17 -1
- package/test/client-logs-route.test.js +34 -0
- package/utils/client-log-file.js +26 -0
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
|
|
5
|
-
|
|
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
|
|
34
|
-
-
|
|
35
|
-
-
|
|
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 "
|
|
74
|
+
ssh do "tail -n 300 /var/log/torrent-tv/client.log"
|
|
38
75
|
# filter a single viewing:
|
|
39
|
-
ssh do "
|
|
40
|
-
ssh do "
|
|
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 (
|
|
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
|
-
|
|
70
|
-
addon host), named so the two
|
|
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
|
@@ -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
|
|
633
|
-
//
|
|
634
|
-
|
|
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
|
+
});
|
package/utils/client-log-file.js
CHANGED
|
@@ -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
|
|