@torrent-tv/proxy 2.88.0 → 2.88.1

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.
@@ -1,133 +1,133 @@
1
- # Browser–proxy playback contract
2
-
3
- This document records the HTTP contract used by the browser playback code and
4
- implemented by this proxy. The same paths and payloads pass through either the
5
- direct HTTP transport or the WebRTC data channel. It is a description of the
6
- current code, not a generated schema or a version negotiation protocol.
7
-
8
- ## Source and file discovery
9
-
10
- | Request | Required input | Successful answer |
11
- |---|---|---|
12
- | `POST /api/sources` | `{ sourceType, source }` | `{ sourceKey }` identifies the registered source. |
13
- | `GET /api/sources/:sourceKey/files?maxWaitMs=N` | Registered `sourceKey`; optional wait budget | While magnet metadata is unavailable: `{ pending: true }`. When ready: `{ name, infoHash, files, items }`. `files` carries `fileIndex`, `name`, `relativePath`, `length`, and `kind`; `items` maps each video `fileIndex` to audio, subtitle, and image file indices. |
14
- | `GET /api/sources/:sourceKey/stats?fileIndex=N` | Registered source; optional file index | Current peer, transfer, header, and file progress readings. A reading may not contain every measurement; absent or `null` measurements are unknown. |
15
- | `POST /api/sources/:sourceKey/warm` | Optional `{ fileIndex, positionSeconds }`; an empty object warms source metadata and the first video | `{ started, swarm, edges, sidecars }` reports which preparation started. Work continues in the background. |
16
- | `GET /api/link-probe?bytes=N` | Positive byte count | A no-store binary body of zero bytes, capped at 2 MiB. The browser measures completed transfer time; an unavailable route leaves link speed unknown. |
17
-
18
- `pending: true` means the requested metadata or plan is not ready yet. The
19
- browser polls again. It does not mean that the torrent has no video or that the
20
- request failed. A non-pending file-list answer uses the proxy's file indices;
21
- the browser must keep those indices when opening a file.
22
-
23
- ## Playback plan and transcode session
24
-
25
- `POST /api/playback-plan` takes `{ sourceKey, fileIndex, userAgent }`. Its answer
26
- contains `mode`, `directUrl`, codec and container names, duration and source
27
- dimensions, track inventories, paired sidecar files, `offeredHeights`,
28
- `audioTracksPending`, and `pending`. The browser uses the codec fields to make
29
- the direct-versus-transcode decision itself; `mode` is advisory.
30
-
31
- When `audioTracksPending` is `true`, one or more sidecar headers were still
32
- unavailable. The browser may call `POST /api/playback-plan/audio-tracks` with
33
- `{ sourceKey, fileIndex }`; the answer contains the current `audioTracks` and
34
- `pending`. A resolved header updates metadata on the already-published first
35
- track for that sidecar. It never changes track indices or the number of HLS
36
- renditions in the active plan.
37
-
38
- `sidecarSubtitles` lists subtitle files the proxy paired with this video. Each
39
- entry carries the torrent `fileIndex` and the proxy's filename-derived language
40
- and release metadata. The browser uses these entries to fetch subtitle files
41
- and to identify a remembered choice. `sidecarImages` is returned beside it but
42
- is not needed for playback. Older proxies and plans made with transcoding
43
- disabled may omit these fields; the browser treats a missing or non-array list
44
- as empty.
45
-
46
- The browser maps a missing `mode` to `direct`, uses its registered `/stream`
47
- URL when `directUrl` is absent, maps missing codec/container names to empty
48
- strings, maps missing duration or dimensions to `0`, and maps missing audio or
49
- subtitle track arrays to `[]`. It maps missing or non-object `offeredHeights` to
50
- `null`; for an object value, missing or invalid `copy` and `transcode` lists
51
- become empty arrays. `null` means no offer was supplied, while empty lists
52
- explicitly offer no heights for their respective playback branch. Only
53
- `pending: true` asks the browser to poll again.
54
-
55
- `POST /api/transcode-sessions` requires `sourceKey`, `fileIndex`, and
56
- `consumerId`. The browser also sends the requested audio/video transcode flags,
57
- the visible picture size, whether audio is requested as separate renditions,
58
- the start position, selected audio track, and segment format when known. The
59
- answer contains `sessionId`, `playlistPath`, `offeredHeights`, declared `tracks`,
60
- and `lookaheadSeconds`. `masterPath` and `variantHeight` are present only when
61
- the session has quality variants.
62
-
63
- The browser interprets `409` with `outcome: "output-unavailable"` as a refusal
64
- for this viewer's link and `outcome: "no-capacity"` as a proxy capacity
65
- refusal. Other non-success statuses remain errors; an error body only adds
66
- display detail and is not required for status handling.
67
-
68
- ## Session control and media files
69
-
70
- | Request | Meaning |
71
- |---|---|
72
- | `POST /api/transcode-sessions/:id/seek` | `{ positionSeconds, consumerId, generation }` reports the viewer's seek. A successful answer is `204`; the proxy handles the seek server side. |
73
- | `GET /api/transcode-sessions/:id/progress?consumer=:id` | Reads current session progress and also keeps the viewer's session alive. `playbackReadiness` is the proxy's versioned forecast used to release the startup wait; missing or unsupported versions do not authorize playback. An unreadable or failed answer does not imply that playback has ended. |
74
- | `POST /api/transcode-sessions/:id/net-report` | `{ consumerId, bufferedAheadSec, ... }` reports the viewer's buffer and optional state/link measurements. `consumerId` and non-negative `bufferedAheadSec` are required. `linkMbps` is the smoothed estimate used by the quality budget; `linkSampleMbps` and `linkSampleAt` carry the newest raw delivery measurement and its sample time for the readiness forecast. `bufferLimitSeconds` states the browser's accepted forward-buffer ceiling. Missing optional values do not reject the report or erase the last known value. Missing `playing` becomes `false`; missing `waiting` is derived from the playback and buffer state. Missing `positionSeconds` leaves the position unchanged, missing `qualityMode` leaves the prior mode, `onScreen` defaults to `true`, and `inPictureInPicture` defaults to `false`. Success is `204`. |
75
- | `POST /api/transcode-sessions/:id/fragment-far` | Diagnostic fragment and buffer positions; does not change encoding. Best effort, success `204`. |
76
- | `POST /api/transcode-sessions/:id/release` | `{ consumerId, reason }` releases this viewer's session assignment. |
77
- | `GET /transcode/:id/:fileName` | HLS playlist, init, or media segment. A file that is still being produced is answered with retryable `503`, not `202`. |
78
- | `GET /transcode/:id/v/:height/warm` and `/transcode/:id/a/:track/warm` | Prepare the requested video height or audio track at a position. `204` means ready. Audio warm-up `404` means the proxy does not support this operation for that session. |
79
- | `GET /stream?sourceKey=…&fileIndex=N` | Direct file bytes with HTTP range support. |
80
-
81
- ### Playback readiness forecast
82
-
83
- The progress response may include `playbackReadiness`:
84
-
85
- | Field | Meaning |
86
- |---|---|
87
- | `version` | Forecast contract version. The browser releases its startup wait only for version `1` with `ready: true`. |
88
- | `ready` | The proxy's simulated buffer trajectory can cover the remaining playback while preserving the measured interruption reserve, starting now. |
89
- | `delaySeconds` | When `ready` is false, the predicted minimum delay from now to a safe start; `null` means the current measurements do not support a finite forecast. |
90
- | `bufferedSeconds` | Client buffer reported to the proxy. |
91
- | `reserveSeconds` | Maximum interruption reserve across the required video and selected audio outputs, derived from each output's measured supply waits and next segment duration. |
92
- | `neededSeconds` | Reserve shortfall at the current client buffer. |
93
- | `reason` | Machine-readable result of the forecast. |
94
- | `preparedSegments` | Number of already-produced output segments available across required tracks. |
95
- | `bufferedAtStartSeconds` | Forecast playable buffer at the proposed start time, when a finite delay is available. |
96
-
97
- The forecast considers the video output and the selected audio output when audio
98
- is delivered separately. A mixed output can depend on more than one source
99
- file. Repeated source files are counted once; distinct files keep separate
100
- remaining-work estimates and share the measured download rate of their torrent.
101
- The browser reports buffer state and delivery measurements, but does not
102
- calculate or override readiness.
103
-
104
- Service work is the integral of a nonnegative measured-rate forecast. The latest
105
- rate relaxes over the observed measurement span toward the lower of its latest
106
- value and the recent mean; the measurement history covers the output look-ahead.
107
- A positive measured rate cannot become permanently zero from an old declining
108
- linear fit. A measured zero remains zero until a new measurement arrives.
109
- The reported loader limit is bounded below by the browser buffer already held.
110
- During startup waiting the browser refreshes stale delivery measurements through
111
- the existing link probe and reports their age to avoid clock-offset errors.
112
-
113
- ## Subtitles
114
-
115
- `GET /api/subtitles` requires `sourceKey` and `fileIndex`. Without `trackIndex`
116
- it reads a paired sidecar file. With `trackIndex`, it reads cues from an
117
- embedded text track and may take `consumerId` to subscribe that viewer to
118
- subsequent cue pushes. `since` asks for cues after a previously received
119
- cursor.
120
-
121
- `200` returns WebVTT. Embedded cue answers may include
122
- `X-Subtitle-Cursor`, `X-Subtitle-Covered-Clusters`, and
123
- `X-Subtitle-Indexed-Clusters`; language detection may include
124
- `X-Subtitle-Language` and `X-Subtitle-Language-Name`. While an embedded track
125
- is being extracted, the response is `202` with `{ pending: true }`; the browser
126
- waits and repeats the request. A missing cursor or language header means that
127
- the browser has no value to preserve or display yet. A failed sidecar or
128
- embedded-track request skips that track without failing video playback.
129
-
130
- Subtitle cue pushes are delivered through the established proxy connection,
131
- not by a separate browser polling route. After reconnect, the browser repeats
132
- the embedded-track request with `since` to restore the subscription and receive
133
- missed cues.
1
+ # Browser–proxy playback contract
2
+
3
+ This document records the HTTP contract used by the browser playback code and
4
+ implemented by this proxy. The same paths and payloads pass through either the
5
+ direct HTTP transport or the WebRTC data channel. It is a description of the
6
+ current code, not a generated schema or a version negotiation protocol.
7
+
8
+ ## Source and file discovery
9
+
10
+ | Request | Required input | Successful answer |
11
+ |---|---|---|
12
+ | `POST /api/sources` | `{ sourceType, source }` | `{ sourceKey }` identifies the registered source. |
13
+ | `GET /api/sources/:sourceKey/files?maxWaitMs=N` | Registered `sourceKey`; optional wait budget | While magnet metadata is unavailable: `{ pending: true }`. When ready: `{ name, infoHash, files, items, shape }`. `files` carries `fileIndex`, `name`, `relativePath`, `length`, and `kind`; `items` maps each video `fileIndex` to audio, subtitle, and image file indices and carries `episode` — what the name states in the release's own numbering (`season`, `episodes`, `part`, `special`, `showHint`, `titleHint`) or `null`. `shape` is `single`, `series` or `undetermined` (2.88.0); an older proxy sends neither field. |
14
+ | `GET /api/sources/:sourceKey/stats?fileIndex=N` | Registered source; optional file index | Current peer, transfer, header, and file progress readings. A reading may not contain every measurement; absent or `null` measurements are unknown. |
15
+ | `POST /api/sources/:sourceKey/warm` | Optional `{ fileIndex, positionSeconds }`; an empty object warms source metadata and the first video | `{ started, swarm, edges, sidecars }` reports which preparation started. Work continues in the background. |
16
+ | `GET /api/link-probe?bytes=N` | Positive byte count | A no-store binary body of zero bytes, capped at 2 MiB. The browser measures completed transfer time; an unavailable route leaves link speed unknown. |
17
+
18
+ `pending: true` means the requested metadata or plan is not ready yet. The
19
+ browser polls again. It does not mean that the torrent has no video or that the
20
+ request failed. A non-pending file-list answer uses the proxy's file indices;
21
+ the browser must keep those indices when opening a file.
22
+
23
+ ## Playback plan and transcode session
24
+
25
+ `POST /api/playback-plan` takes `{ sourceKey, fileIndex, userAgent }`. Its answer
26
+ contains `mode`, `directUrl`, codec and container names, duration and source
27
+ dimensions, track inventories, paired sidecar files, `offeredHeights`,
28
+ `audioTracksPending`, and `pending`. The browser uses the codec fields to make
29
+ the direct-versus-transcode decision itself; `mode` is advisory.
30
+
31
+ When `audioTracksPending` is `true`, one or more sidecar headers were still
32
+ unavailable. The browser may call `POST /api/playback-plan/audio-tracks` with
33
+ `{ sourceKey, fileIndex }`; the answer contains the current `audioTracks` and
34
+ `pending`. A resolved header updates metadata on the already-published first
35
+ track for that sidecar. It never changes track indices or the number of HLS
36
+ renditions in the active plan.
37
+
38
+ `sidecarSubtitles` lists subtitle files the proxy paired with this video. Each
39
+ entry carries the torrent `fileIndex` and the proxy's filename-derived language
40
+ and release metadata. The browser uses these entries to fetch subtitle files
41
+ and to identify a remembered choice. `sidecarImages` is returned beside it but
42
+ is not needed for playback. Older proxies and plans made with transcoding
43
+ disabled may omit these fields; the browser treats a missing or non-array list
44
+ as empty.
45
+
46
+ The browser maps a missing `mode` to `direct`, uses its registered `/stream`
47
+ URL when `directUrl` is absent, maps missing codec/container names to empty
48
+ strings, maps missing duration or dimensions to `0`, and maps missing audio or
49
+ subtitle track arrays to `[]`. It maps missing or non-object `offeredHeights` to
50
+ `null`; for an object value, missing or invalid `copy` and `transcode` lists
51
+ become empty arrays. `null` means no offer was supplied, while empty lists
52
+ explicitly offer no heights for their respective playback branch. Only
53
+ `pending: true` asks the browser to poll again.
54
+
55
+ `POST /api/transcode-sessions` requires `sourceKey`, `fileIndex`, and
56
+ `consumerId`. The browser also sends the requested audio/video transcode flags,
57
+ the visible picture size, whether audio is requested as separate renditions,
58
+ the start position, selected audio track, and segment format when known. The
59
+ answer contains `sessionId`, `playlistPath`, `offeredHeights`, declared `tracks`,
60
+ and `lookaheadSeconds`. `masterPath` and `variantHeight` are present only when
61
+ the session has quality variants.
62
+
63
+ The browser interprets `409` with `outcome: "output-unavailable"` as a refusal
64
+ for this viewer's link and `outcome: "no-capacity"` as a proxy capacity
65
+ refusal. Other non-success statuses remain errors; an error body only adds
66
+ display detail and is not required for status handling.
67
+
68
+ ## Session control and media files
69
+
70
+ | Request | Meaning |
71
+ |---|---|
72
+ | `POST /api/transcode-sessions/:id/seek` | `{ positionSeconds, consumerId, generation }` reports the viewer's seek. A successful answer is `204`; the proxy handles the seek server side. |
73
+ | `GET /api/transcode-sessions/:id/progress?consumer=:id` | Reads current session progress and also keeps the viewer's session alive. `playbackReadiness` is the proxy's versioned forecast used to release the startup wait; missing or unsupported versions do not authorize playback. An unreadable or failed answer does not imply that playback has ended. |
74
+ | `POST /api/transcode-sessions/:id/net-report` | `{ consumerId, bufferedAheadSec, ... }` reports the viewer's buffer and optional state/link measurements. `consumerId` and non-negative `bufferedAheadSec` are required. `linkMbps` is the smoothed estimate used by the quality budget; `linkSampleMbps` and `linkSampleAt` carry the newest raw delivery measurement and its sample time for the readiness forecast. `bufferLimitSeconds` states the browser's accepted forward-buffer ceiling. Missing optional values do not reject the report or erase the last known value. Missing `playing` becomes `false`; missing `waiting` is derived from the playback and buffer state. Missing `positionSeconds` leaves the position unchanged, missing `qualityMode` leaves the prior mode, `onScreen` defaults to `true`, and `inPictureInPicture` defaults to `false`. Success is `204`. |
75
+ | `POST /api/transcode-sessions/:id/fragment-far` | Diagnostic fragment and buffer positions; does not change encoding. Best effort, success `204`. |
76
+ | `POST /api/transcode-sessions/:id/release` | `{ consumerId, reason }` releases this viewer's session assignment. |
77
+ | `GET /transcode/:id/:fileName` | HLS playlist, init, or media segment. A file that is still being produced is answered with retryable `503`, not `202`. |
78
+ | `GET /transcode/:id/v/:height/warm` and `/transcode/:id/a/:track/warm` | Prepare the requested video height or audio track at a position. `204` means ready. Audio warm-up `404` means the proxy does not support this operation for that session. |
79
+ | `GET /stream?sourceKey=…&fileIndex=N` | Direct file bytes with HTTP range support. |
80
+
81
+ ### Playback readiness forecast
82
+
83
+ The progress response may include `playbackReadiness`:
84
+
85
+ | Field | Meaning |
86
+ |---|---|
87
+ | `version` | Forecast contract version. The browser releases its startup wait only for version `1` with `ready: true`. |
88
+ | `ready` | The proxy's simulated buffer trajectory can cover the remaining playback while preserving the measured interruption reserve, starting now. |
89
+ | `delaySeconds` | When `ready` is false, the predicted minimum delay from now to a safe start; `null` means the current measurements do not support a finite forecast. |
90
+ | `bufferedSeconds` | Client buffer reported to the proxy. |
91
+ | `reserveSeconds` | Maximum interruption reserve across the required video and selected audio outputs, derived from each output's measured supply waits and next segment duration. |
92
+ | `neededSeconds` | Reserve shortfall at the current client buffer. |
93
+ | `reason` | Machine-readable result of the forecast. |
94
+ | `preparedSegments` | Number of already-produced output segments available across required tracks. |
95
+ | `bufferedAtStartSeconds` | Forecast playable buffer at the proposed start time, when a finite delay is available. |
96
+
97
+ The forecast considers the video output and the selected audio output when audio
98
+ is delivered separately. A mixed output can depend on more than one source
99
+ file. Repeated source files are counted once; distinct files keep separate
100
+ remaining-work estimates and share the measured download rate of their torrent.
101
+ The browser reports buffer state and delivery measurements, but does not
102
+ calculate or override readiness.
103
+
104
+ Service work is the integral of a nonnegative measured-rate forecast. The latest
105
+ rate relaxes over the observed measurement span toward the lower of its latest
106
+ value and the recent mean; the measurement history covers the output look-ahead.
107
+ A positive measured rate cannot become permanently zero from an old declining
108
+ linear fit. A measured zero remains zero until a new measurement arrives.
109
+ The reported loader limit is bounded below by the browser buffer already held.
110
+ During startup waiting the browser refreshes stale delivery measurements through
111
+ the existing link probe and reports their age to avoid clock-offset errors.
112
+
113
+ ## Subtitles
114
+
115
+ `GET /api/subtitles` requires `sourceKey` and `fileIndex`. Without `trackIndex`
116
+ it reads a paired sidecar file. With `trackIndex`, it reads cues from an
117
+ embedded text track and may take `consumerId` to subscribe that viewer to
118
+ subsequent cue pushes. `since` asks for cues after a previously received
119
+ cursor.
120
+
121
+ `200` returns WebVTT. Embedded cue answers may include
122
+ `X-Subtitle-Cursor`, `X-Subtitle-Covered-Clusters`, and
123
+ `X-Subtitle-Indexed-Clusters`; language detection may include
124
+ `X-Subtitle-Language` and `X-Subtitle-Language-Name`. While an embedded track
125
+ is being extracted, the response is `202` with `{ pending: true }`; the browser
126
+ waits and repeats the request. A missing cursor or language header means that
127
+ the browser has no value to preserve or display yet. A failed sidecar or
128
+ embedded-track request skips that track without failing video playback.
129
+
130
+ Subtitle cue pushes are delivered through the established proxy connection,
131
+ not by a separate browser polling route. After reconnect, the browser repeats
132
+ the embedded-track request with `since` to restore the subscription and receive
133
+ missed cues.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.88.0",
3
+ "version": "2.88.1",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -147,7 +147,24 @@ ${clean}`;
147
147
  return null;
148
148
  }
149
149
  const cues = new SubtitleFileContainer({ extension: ext }).readCues(clean);
150
- return cues === null ? null : TextSubtitleTrack.cuesToVtt(cues, ext);
150
+ if (cues === null) return null;
151
+ const vtt = TextSubtitleTrack.cuesToVtt(cues, ext);
152
+ if (ext !== ".ass" && ext !== ".ssa") return vtt;
153
+ const header = clean.slice(0, 16384).match(/\[Script Info\]([^]*?)(?=\n\[|$)/iu)?.[1] ?? "";
154
+ const metadata = { titles: [], genericTitles: [], years: [] };
155
+ for (const line of header.split(/\r?\n/u)) {
156
+ const field = line.match(/^\s*(Title|Movie Title|Series Title|Original Title|Year|Release Year)\s*:\s*(.{1,160})$/iu);
157
+ if (!field) continue;
158
+ const key = field[1].toLowerCase();
159
+ const value = field[2].trim();
160
+ if (key.includes("year")) {
161
+ if (/^(18|19|20)\d{2}$/u.test(value)) metadata.years.push(Number(value));
162
+ } else if (key === "title") metadata.genericTitles.push(value);
163
+ else metadata.titles.push(value);
164
+ }
165
+ for (const key of Object.keys(metadata)) metadata[key] = [...new Set(metadata[key])].slice(0, 4);
166
+ if (!Object.values(metadata).some(values => values.length)) return vtt;
167
+ return vtt.replace(/^WEBVTT\r?\n\r?\n/u, `WEBVTT\n\nNOTE TORRENT-TV-METADATA\n${JSON.stringify(metadata)}\n\n`);
151
168
  }
152
169
 
153
170
  static detect(extension) {
@@ -0,0 +1,15 @@
1
+ import test from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import { SubtitleFileContainer } from "../services/media/container/SubtitleFileContainer.js";
4
+
5
+ const ass = `[Script Info]\nTitle: Princess Mononoke\nYear: 1997\nMovie Title: Princess Mononoke\nScript Updated By: 2026\n\n[Events]\nFormat: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text\nDialogue: 0,0:00:01.00,0:00:02.00,Default,,0,0,0,,Hello`;
6
+ test("ASS header evidence survives conversion without changing cues", () => {
7
+ const vtt = SubtitleFileContainer.toVtt(ass, ".ass");
8
+ const json = vtt.match(/NOTE TORRENT-TV-METADATA\n([^\n]+)/u)[1];
9
+ assert.deepEqual(JSON.parse(json), { titles: ["Princess Mononoke"], genericTitles: ["Princess Mononoke"], years: [1997] });
10
+ assert.match(vtt, /00:00:01.000 --> 00:00:02.000\nHello/u);
11
+ });
12
+ test("metadata is bounded and dialogue is not scanned", () => {
13
+ const vtt = SubtitleFileContainer.toVtt(ass.replace("[Script Info]", "[Other]").replace("Hello", "Movie Title: Another film"), ".ass");
14
+ assert.equal(vtt.includes("NOTE TORRENT-TV-METADATA"), false);
15
+ });