@torrent-tv/proxy 2.50.0 → 2.52.0

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.
@@ -0,0 +1,72 @@
1
+ /**
2
+ * @file The DHT's entry points are given as addresses, not names.
3
+ *
4
+ * Measured 2026-08-21 on the addon host. Two of the three bootstrap nodes the
5
+ * library ships answer nothing: `router.bittorrent.com` and `router.utorrent.com`
6
+ * did not reply to a hand-written `ping` at all, while a control datagram to a
7
+ * DNS server came back in 20 ms. The third, `dht.transmissionbt.com`, is alive —
8
+ * it answered `find_node` with eight nodes — but on a host with global IPv6 its
9
+ * name resolves to an IPv6 address first, and the DHT's socket is IPv4, so by
10
+ * name it was never reached. By name: 0 nodes after 21 s, every run. By address:
11
+ * 22 nodes in 5 s.
12
+ */
13
+
14
+ import assert from "node:assert/strict";
15
+ import test from "node:test";
16
+
17
+ import { dhtNodeCount, parseBootstrapEntry, resolveDhtBootstrap } from "../services/torrent-pool.js";
18
+
19
+ test("a bootstrap entry is split into host and port", () => {
20
+ assert.deepEqual(parseBootstrapEntry("dht.libtorrent.org:25401"), {
21
+ host: "dht.libtorrent.org",
22
+ port: 25401
23
+ });
24
+ });
25
+
26
+ test("an entry with no port gets the DHT's own", () => {
27
+ assert.deepEqual(parseBootstrapEntry("router.bittorrent.com"), {
28
+ host: "router.bittorrent.com",
29
+ port: 6881
30
+ });
31
+ });
32
+
33
+ test("an unusable entry is dropped rather than breaking the client", () => {
34
+ // One bad line in the list must cost that node and nothing else — the DHT is
35
+ // best-effort by nature and a typo here would otherwise take the whole client
36
+ // down at construction.
37
+ assert.equal(parseBootstrapEntry(""), null);
38
+ assert.equal(parseBootstrapEntry(" "), null);
39
+ assert.equal(parseBootstrapEntry("host:0"), null);
40
+ assert.equal(parseBootstrapEntry("host:70000"), null);
41
+ assert.equal(parseBootstrapEntry("host:not-a-port"), null);
42
+ assert.equal(parseBootstrapEntry(":6881"), null);
43
+ assert.equal(parseBootstrapEntry(null), null);
44
+ assert.equal(parseBootstrapEntry(42), null);
45
+ });
46
+
47
+ test("the routing table's size is readable, and its absence is not an error", () => {
48
+ assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [1, 2, 3] } } }), 3);
49
+ assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => [] } } }), 0);
50
+ // A client built without a DHT, or one whose internals moved: the difference
51
+ // between "no DHT" and "an empty DHT" is the whole point of the report, so
52
+ // the first must not be printed as the second.
53
+ assert.equal(dhtNodeCount({}), null);
54
+ assert.equal(dhtNodeCount({ dht: {} }), null);
55
+ assert.equal(dhtNodeCount(null), null);
56
+ assert.equal(dhtNodeCount({ dht: { nodes: { toArray: () => { throw new Error("gone"); } } } }), null);
57
+ });
58
+
59
+ test("a name that will not resolve inside the cap is dropped, not waited on", async () => {
60
+ // The whole call is awaited before the torrent client exists, so a resolver
61
+ // that black-holes must cost the cap and not c-ares' own four tries.
62
+ const started = Date.now();
63
+ // `.invalid` is reserved by RFC 2606 and never resolves; the cap is what
64
+ // bounds the wait when a resolver answers slowly rather than quickly.
65
+ const resolved = await resolveDhtBootstrap(["nothing.invalid:6881"], 200);
66
+ assert.deepEqual(resolved, []);
67
+ assert.ok(Date.now() - started < 2000, "the cap, not the resolver, decided when to give up");
68
+ });
69
+
70
+ test("an empty list is an answer, not a failure", async () => {
71
+ assert.deepEqual(await resolveDhtBootstrap([], 200), []);
72
+ });
@@ -0,0 +1,87 @@
1
+ /**
2
+ * @file A run is POSITIONED where the player was told the segment begins.
3
+ *
4
+ * The companion of `cuts-follow-published-grid`, and the half that was missing.
5
+ * 2.45.0 moved the cut list onto the published table and left the position on
6
+ * the live one — but `-segment_times` are measured from wherever the run really
7
+ * began, so any distance between the two moves EVERY cut of that run by it. The
8
+ * live table keeps being corrected, and the corrections run backwards, so each
9
+ * restart began a little earlier than the grid its cuts were stated on and the
10
+ * distance accumulated across restarts.
11
+ *
12
+ * Field 2026-08-21, `JUFD665.mp4` (MP4, copy path, index read cleanly): after
13
+ * one seek restart a produced segment held the boundary two places before its
14
+ * own number — 16.684 s, which is 2.0000 segments — and after the next restart,
15
+ * four places, 33.5 s. The player's buffer then stopped extending at all,
16
+ * because every fragment's content landed before the time its playlist entry
17
+ * named: `bufferEnd` stood still at 4571.1 s through four `frag-far` warnings
18
+ * until hls.js gave up and jumped the viewer 16.8 s forward.
19
+ */
20
+
21
+ import assert from "node:assert/strict";
22
+ import test from "node:test";
23
+
24
+ import { HlsSessionManager, describeGridDrift, segmentCutTimesFrom } from "../services/hls-session-manager.js";
25
+
26
+ /** What the playlist in the player's hands says. */
27
+ const PUBLISHED = [0, 8.342, 16.684, 25.026, 33.368, 41.71];
28
+ /** The same grid after produced segments moved two of its cuts backwards. */
29
+ const CORRECTED = [0, 8.342, 14.682, 25.026, 31.366, 41.71];
30
+
31
+ /**
32
+ * @returns {{ manager: HlsSessionManager, session: object }}
33
+ */
34
+ function sessionWithDriftedGrid() {
35
+ const manager = new HlsSessionManager({
36
+ enabled: true,
37
+ ffmpegBin: "ffmpeg",
38
+ localBindHost: "127.0.0.1",
39
+ localPort: 9090
40
+ });
41
+ const session = {
42
+ id: "picture",
43
+ segmentBoundaries: [...CORRECTED],
44
+ publishedBoundaries: [...PUBLISHED]
45
+ };
46
+ return { manager, session };
47
+ }
48
+
49
+ test("a run starts at the time the player was told, not at the corrected one", () => {
50
+ const { manager, session } = sessionWithDriftedGrid();
51
+ assert.equal(manager.runStartTimeFor(session, 2), 16.684);
52
+ assert.notEqual(manager.runStartTimeFor(session, 2), session.segmentBoundaries[2]);
53
+ });
54
+
55
+ test("position and cut list come from the same table", () => {
56
+ const { manager, session } = sessionWithDriftedGrid();
57
+ const grid = manager.publishedGridFor(session);
58
+ const start = manager.runStartTimeFor(session, 2);
59
+ // The cut list is stated as offsets from where the run begins. Adding the
60
+ // position back must land on the published boundaries exactly — which is the
61
+ // property that was false while the two came from different tables.
62
+ const absolute = segmentCutTimesFrom(grid, 2).map((offset) => Number((start + offset).toFixed(3)));
63
+ assert.deepEqual(absolute, [25.026, 33.368]);
64
+ });
65
+
66
+ test("a session that published no grid positions on the live one", () => {
67
+ const { manager } = sessionWithDriftedGrid();
68
+ const session = { id: "no-playlist", segmentBoundaries: [...CORRECTED], publishedBoundaries: [] };
69
+ assert.equal(manager.runStartTimeFor(session, 2), CORRECTED[2]);
70
+ });
71
+
72
+ test("an index beyond the table is clamped rather than returning nothing", () => {
73
+ const { manager, session } = sessionWithDriftedGrid();
74
+ assert.equal(manager.runStartTimeFor(session, 9999), PUBLISHED[PUBLISHED.length - 1]);
75
+ assert.equal(manager.runStartTimeFor(session, -3), PUBLISHED[0]);
76
+ });
77
+
78
+ test("the drift between the two tables is stated in full", () => {
79
+ assert.equal(describeGridDrift(PUBLISHED, PUBLISHED), "identical");
80
+ assert.equal(
81
+ describeGridDrift(PUBLISHED, CORRECTED),
82
+ "2 of 6 boundaries apart, worst 2.002s at #2"
83
+ );
84
+ assert.equal(describeGridDrift(PUBLISHED, [0, 1]), "a different length (6 against 2)");
85
+ assert.equal(describeGridDrift(null, CORRECTED), "not comparable");
86
+ assert.equal(describeGridDrift([], []), "not comparable");
87
+ });
@@ -0,0 +1,125 @@
1
+ /**
2
+ * @file The line describing a swarm answers the question it is asked.
3
+ *
4
+ * It could not. `routes/api/sources/stats/get.js` printed `peers=N` beside
5
+ * `wires=?`, which reads as two quantities of which one is unknown — while
6
+ * WebTorrent's `numPeers` IS `wires.length` (`lib/torrent.js:254`, the same in
7
+ * 2.8.5 and 3.0.21), so the first was the connection count and the second was a
8
+ * field that has never printed anything, because the torrent lives on a worker
9
+ * thread where that property does not exist.
10
+ *
11
+ * What was missing is the other half: how many peers the client HOLDS but is
12
+ * not connected to, and what the tracker said the swarm has. Measured
13
+ * 2026-08-21 on `JUFD665.mp4`, the tracker answered `seeders=5` at 13:40:30 and
14
+ * the first wire arrived at 13:44:47 — 4 min 17 s in which the stats line
15
+ * repeated unchanged every two seconds while the answer ("offered, not
16
+ * connected") was already in the process.
17
+ */
18
+
19
+ import assert from "node:assert/strict";
20
+ import test from "node:test";
21
+
22
+ import { bestAnnounce, describeSwarmReach, secondsToFirstPeer } from "../services/torrent-pool.js";
23
+
24
+ test("connected and known are separate numbers", () => {
25
+ const torrent = { wires: [{}, {}], _peersLength: 7, _numQueued: 4 };
26
+ assert.deepEqual(describeSwarmReach(torrent), {
27
+ connectedPeers: 2,
28
+ knownPeers: 7,
29
+ queuedPeers: 4
30
+ });
31
+ });
32
+
33
+ test("offered but not connected is distinguishable from nobody offered", () => {
34
+ // The two shapes the field produced. They need opposite investigations, and
35
+ // one line has to tell them apart.
36
+ const offeredNotConnected = describeSwarmReach({ wires: [], _peersLength: 5, _numQueued: 5 });
37
+ const nobodyOffered = describeSwarmReach({ wires: [], _peersLength: 0, _numQueued: 0 });
38
+ assert.equal(offeredNotConnected.connectedPeers, 0);
39
+ assert.equal(nobodyOffered.connectedPeers, 0);
40
+ assert.notEqual(offeredNotConnected.knownPeers, nobodyOffered.knownPeers);
41
+ });
42
+
43
+ test("internals that are gone say nothing rather than breaking the poll", () => {
44
+ // `_peersLength` and `_numQueued` are not WebTorrent's published interface.
45
+ // The browser polls this every two seconds, so a version that drops them must
46
+ // cost the field and not the answer.
47
+ assert.deepEqual(describeSwarmReach({ wires: [{}] }), {
48
+ connectedPeers: 1,
49
+ knownPeers: null,
50
+ queuedPeers: null
51
+ });
52
+ assert.deepEqual(describeSwarmReach({}), {
53
+ connectedPeers: 0,
54
+ knownPeers: null,
55
+ queuedPeers: null
56
+ });
57
+ assert.deepEqual(describeSwarmReach(null), {
58
+ connectedPeers: 0,
59
+ knownPeers: null,
60
+ queuedPeers: null
61
+ });
62
+ const throwing = {
63
+ wires: [],
64
+ get _peersLength() {
65
+ throw new Error("destroyed");
66
+ }
67
+ };
68
+ assert.deepEqual(describeSwarmReach(throwing), {
69
+ connectedPeers: 0,
70
+ knownPeers: null,
71
+ queuedPeers: null
72
+ });
73
+ });
74
+
75
+ test("the wait for a first peer is a measured quantity", () => {
76
+ // The field case: added 13:40:30.357, first wire 13:44:47.123.
77
+ assert.equal(secondsToFirstPeer(1000, 258_766), 257.766);
78
+ assert.equal(secondsToFirstPeer(1000, 1000), 0);
79
+ });
80
+
81
+ test("no peer yet is not a duration", () => {
82
+ assert.equal(secondsToFirstPeer(1000, null), null);
83
+ assert.equal(secondsToFirstPeer(null, 2000), null);
84
+ // A clock that went backwards is not a negative wait; it is no reading.
85
+ assert.equal(secondsToFirstPeer(2000, 1000), null);
86
+ });
87
+
88
+ test("the best tracker answer wins, not the most recent", () => {
89
+ // A live tracker says 500, a dead one answers 0 two seconds later. Keeping
90
+ // the last would print "nobody offered" about a swarm of five hundred, which
91
+ // inverts the one distinction this figure is carried for.
92
+ const answers = [
93
+ { seeders: 500, leechers: 40 },
94
+ { seeders: 0, leechers: 0 }
95
+ ];
96
+ assert.deepEqual(bestAnnounce(answers), { seeders: 500, leechers: 40, trackers: 2 });
97
+ });
98
+
99
+ test("trackers that answered are counted even when none knew anything", () => {
100
+ assert.deepEqual(bestAnnounce([{ seeders: null, leechers: null }]), {
101
+ seeders: null,
102
+ leechers: null,
103
+ trackers: 1
104
+ });
105
+ // No tracker has answered at all — a different state from "answered, knows
106
+ // nobody", and the line says so.
107
+ assert.deepEqual(bestAnnounce([]), { seeders: null, leechers: null, trackers: 0 });
108
+ assert.deepEqual(bestAnnounce(null), { seeders: null, leechers: null, trackers: 0 });
109
+ });
110
+
111
+ test("leechers travel with the seeder count they were reported beside", () => {
112
+ const answers = [
113
+ { seeders: 2, leechers: 99 },
114
+ { seeders: 7, leechers: 3 }
115
+ ];
116
+ assert.equal(bestAnnounce(answers).leechers, 3);
117
+ // Including when the winning answer gave no leecher count: carrying the
118
+ // previous tracker's figure forward would present two trackers' numbers as
119
+ // one reading.
120
+ assert.deepEqual(bestAnnounce([{ seeders: 2, leechers: 99 }, { seeders: 7, leechers: null }]), {
121
+ seeders: 7,
122
+ leechers: null,
123
+ trackers: 2
124
+ });
125
+ });
@@ -0,0 +1,53 @@
1
+ /**
2
+ * @file Opening a file at a position puts the SOUND there too.
3
+ *
4
+ * The audio rendition is a session of its own, and where it starts is computed
5
+ * from where the viewer is. That reading had three sources and only two were
6
+ * consulted — a position seeked to, and the last segment this session served —
7
+ * both of which are written by things that have not happened yet at the moment
8
+ * a file is opened at a position. So the answer was zero.
9
+ *
10
+ * Field 2026-08-21, `Minions.and.Monsters.1080p.mkv` reopened from the address
11
+ * bar at 52:07:
12
+ *
13
+ * 18:02:55.124 8ed85605 start=3130s -ss 3125.25 -an -map 0:v:0 -c:v copy
14
+ * 18:02:55.632 33b0b046 start=0s no -ss -vn -map 0:a:0 -c:a aac
15
+ *
16
+ * The picture went to segment #781, the sound to #0. The player asked both for
17
+ * #782; the picture had it, the sound re-encoded 57.5 s of a 3130 s film in the
18
+ * 45 s the request lasted and then answered 404 — shown to the viewer as "the
19
+ * proxy accepted the request but sent no video".
20
+ */
21
+
22
+ import assert from "node:assert/strict";
23
+ import test from "node:test";
24
+
25
+ import { resolveViewerPosition } from "../services/hls-session-manager.js";
26
+
27
+ test("a file opened at a position has its viewer at that position", () => {
28
+ // Nothing has been seeked and nothing served yet — the state at the instant
29
+ // the audio rendition is created.
30
+ assert.equal(resolveViewerPosition({ openedAt: 3130 }), 3130);
31
+ });
32
+
33
+ test("a seek beats everything else", () => {
34
+ assert.equal(
35
+ resolveViewerPosition({ seeked: 900, lastRequestedStart: 400, openedAt: 3130 }),
36
+ 900
37
+ );
38
+ });
39
+
40
+ test("what has been served beats where the file was opened", () => {
41
+ // The opening position is the oldest of the three readings: once a segment
42
+ // has been served, that is where the reading is.
43
+ assert.equal(resolveViewerPosition({ lastRequestedStart: 400, openedAt: 3130 }), 400);
44
+ });
45
+
46
+ test("with nothing to go on the answer is the beginning", () => {
47
+ assert.equal(resolveViewerPosition({}), 0);
48
+ assert.equal(resolveViewerPosition({ seeked: 0, lastRequestedStart: 0, openedAt: 0 }), 0);
49
+ // Values that are not readings must not become one.
50
+ assert.equal(resolveViewerPosition({ seeked: Number.NaN, openedAt: Number.NaN }), 0);
51
+ assert.equal(resolveViewerPosition({ seeked: -5, openedAt: -5 }), 0);
52
+ assert.equal(resolveViewerPosition({ lastRequestedStart: null, openedAt: undefined }), 0);
53
+ });