@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.
- package/CHANGELOG.md +2242 -2238
- package/docs/browser-proxy-contract.md +133 -133
- package/package.json +1 -1
- package/services/media/container/SubtitleFileContainer.js +18 -1
- package/test/subtitle-metadata.test.js +15 -0
|
@@ -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
|
@@ -147,7 +147,24 @@ ${clean}`;
|
|
|
147
147
|
return null;
|
|
148
148
|
}
|
|
149
149
|
const cues = new SubtitleFileContainer({ extension: ext }).readCues(clean);
|
|
150
|
-
|
|
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
|
+
});
|