@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.43.0",
3
+ "version": "2.43.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": {
@@ -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
- const held = await cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, trackIndex);
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
- const vtt = cuesToVtt(held.cues, held.codecId);
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;