@torrent-tv/proxy 2.43.0 → 2.43.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 +4 -0
- package/package.json +1 -1
- package/routes/api/subtitles/get.js +21 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
## 2.43.1
|
|
2
|
+
|
|
3
|
+
- **New**: A subtitle request can say where the browser's copy ends (`?after=<seconds>`) and gets back only the cues past it. A track read out of downloaded clusters grows as the film does, and the browser was being sent all of it every few seconds — 76 KB a time on the field file — for the few lines at its end. The language is still detected from every cue held rather than from the handful being sent, because three lines say much less about a language than a whole track does.
|
|
4
|
+
|
|
1
5
|
## 2.43.0
|
|
2
6
|
|
|
3
7
|
- **New**: An MP4's text subtitles are read the same way, and more cheaply than Matroska's. Where a Matroska cue costs whatever cluster holds it — the picture around it included — an MP4 states every sample's own byte range in its sample table (ISO/IEC 14496-12 §8.6.1.2, §8.7.3-8.7.5), so a cue costs its own few dozen bytes and nothing else. The tables are read out of the `moov` the keyframe reader already fetches: `stts` for when each cue starts and how long it lasts, `stsz` for its length, `stsc` with `stco`/`co64` for where its bytes are. `tx3g` (3GPP timed text) and `wvtt` (WebVTT in MP4) are decoded; `stpp` (TTML) is XML and is deliberately left out rather than half-shown. An empty sample is the format's way of saying nothing is on screen and is not turned into a blank cue. Same rule as before: only samples whose bytes are already downloaded are read, so a cue never costs a request.
|
package/package.json
CHANGED
|
@@ -100,7 +100,19 @@ export async function handleApiSubtitlesGet(req, reply, { sourceRegistry, torren
|
|
|
100
100
|
// read that way. Costs no network at all and answers with the part of the
|
|
101
101
|
// film they are watching; the rest arrives as they watch it. Only when the
|
|
102
102
|
// container cannot be read this way does the old extraction run.
|
|
103
|
-
|
|
103
|
+
// Where the browser's copy of this track ends. It already holds every cue
|
|
104
|
+
// before this second, so sending them again is bytes for nothing: measured
|
|
105
|
+
// 2026-08-19, one track is 76 KB and the browser asked for it every few
|
|
106
|
+
// seconds while the film downloaded. Absent or unparsable means "send
|
|
107
|
+
// everything", which is what a browser asking for the first time wants.
|
|
108
|
+
const after = Number.parseFloat(String(req.query?.after ?? ""));
|
|
109
|
+
const held = await cuesFromDownloadedClusters(
|
|
110
|
+
torrentPool,
|
|
111
|
+
torrent,
|
|
112
|
+
fileIndex,
|
|
113
|
+
trackIndex,
|
|
114
|
+
Number.isFinite(after) ? after : null
|
|
115
|
+
);
|
|
104
116
|
if (held !== null) {
|
|
105
117
|
setLanguageHeaders(reply, held.language);
|
|
106
118
|
reply.header("content-type", "text/vtt; charset=utf-8");
|
|
@@ -252,7 +264,7 @@ function readFileFully(file, maxBytes) {
|
|
|
252
264
|
* @returns {Promise<{ vtt: string, language: object | null, coveredClusters: number, indexedClusters: number } | null>}
|
|
253
265
|
* Null when this file cannot be read this way, and then the caller falls back.
|
|
254
266
|
*/
|
|
255
|
-
async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex) {
|
|
267
|
+
async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex, after = null) {
|
|
256
268
|
if (typeof torrentPool?.getSubtitleTracks !== "function") {
|
|
257
269
|
return null;
|
|
258
270
|
}
|
|
@@ -275,7 +287,13 @@ async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, track
|
|
|
275
287
|
if (!held || !Array.isArray(held.cues)) {
|
|
276
288
|
return null;
|
|
277
289
|
}
|
|
278
|
-
|
|
290
|
+
// Only what the browser does not have. The language is still detected from
|
|
291
|
+
// EVERYTHING held, because three new lines say much less about a language
|
|
292
|
+
// than the whole track does.
|
|
293
|
+
const fresh = Number.isFinite(after)
|
|
294
|
+
? held.cues.filter((cue) => cue.startSeconds > after)
|
|
295
|
+
: held.cues;
|
|
296
|
+
const vtt = cuesToVtt(fresh, held.codecId);
|
|
279
297
|
const language = held.cues.length > 0
|
|
280
298
|
? detectLanguage(held.cues.map((cue) => cue.text).join("\n"))
|
|
281
299
|
: null;
|