@torrent-tv/proxy 2.55.14 → 2.57.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.55.14",
3
+ "version": "2.57.0",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -268,9 +268,10 @@ function readFileFully(file, maxBytes) {
268
268
  * The cues of a track from clusters already downloaded, as WebVTT.
269
269
  *
270
270
  * `trackIndex` is the browser's number for the subtitle stream — its position
271
- * among the subtitle streams, as ffprobe lists them — while Matroska blocks
272
- * carry the file's own track number. The plan lists the text tracks in file
273
- * order, so the browser's Nth subtitle stream is the Nth entry.
271
+ * among ALL the subtitle streams, as ffmpeg lists them — while Matroska blocks
272
+ * carry the file's own track number. The plan lists only the tracks that can
273
+ * become text, so it is matched on `declaredIndex`, which each track carries
274
+ * for exactly this: its place in the file's full list of subtitle tracks.
274
275
  *
275
276
  * @param {object} torrentPool
276
277
  * @param {object} torrent
@@ -289,7 +290,15 @@ async function cuesFromDownloadedClusters(torrentPool, torrent, fileIndex, track
289
290
  } catch {
290
291
  return null;
291
292
  }
292
- const track = Array.isArray(tracks) ? tracks[trackIndex] : null;
293
+ // BY the browser's number, not by position in this list. The list holds only
294
+ // the tracks that can become WebVTT, while the browser counts every subtitle
295
+ // stream ffmpeg lists — so on a file carrying a PGS or VobSub track the two
296
+ // ran one apart, and the request either found the wrong track or found none
297
+ // and fell through to the ffmpeg extraction below, which reads the whole film
298
+ // (752 s measured, 2026-08-19) for cues already in hand.
299
+ const track = Array.isArray(tracks)
300
+ ? tracks.find((candidate) => candidate.declaredIndex === trackIndex) ?? null
301
+ : null;
293
302
  if (!track) {
294
303
  return null;
295
304
  }
@@ -63,6 +63,11 @@ const TEXT_CODECS = new Set(["S_TEXT/UTF8", "S_TEXT/ASS", "S_TEXT/SSA"]);
63
63
  /**
64
64
  * @typedef {object} SubtitleTrackPlan
65
65
  * @property {number} trackNumber - As the blocks name it.
66
+ * @property {number} declaredIndex - Its position among ALL of the file's
67
+ * subtitle tracks, picture-based ones included — which is the number ffmpeg
68
+ * gives the same stream in `0:s:N`, and therefore the only number the browser
69
+ * ever names. Text tracks alone are not a numbering: a file whose PGS track
70
+ * comes first would have every text track one lower here than in the browser.
66
71
  * @property {string} codecId
67
72
  * @property {string} language - The three-letter code the file declares.
68
73
  * @property {string} name - What the file calls the track, if anything.
@@ -178,6 +183,7 @@ export async function readSubtitlePlan(readRange, fileSize) {
178
183
  }
179
184
  tracks.push({
180
185
  trackNumber,
186
+ declaredIndex: declared.length - 1,
181
187
  codecId,
182
188
  language,
183
189
  name,
@@ -29,6 +29,15 @@ const MAX_MOOV_BYTES = 32 * 1024 * 1024;
29
29
 
30
30
  /** Handlers that mean "this track is text on screen". */
31
31
  const TEXT_HANDLERS = new Set(["text", "sbtl", "subt"]);
32
+ /**
33
+ * Every handler ffmpeg's mov demuxer turns into a SUBTITLE stream, whether or
34
+ * not this file can read it — `subp` is a DVD subpicture and `clcp` closed
35
+ * captions, both pictures or caption data rather than text. They are counted
36
+ * because `declaredIndex` has to equal ffmpeg's `0:s:N`, and a track left out
37
+ * of the count shifts every text track after it, which is the very defect
38
+ * `declaredIndex` exists to remove.
39
+ */
40
+ const SUBTITLE_HANDLERS = new Set([...TEXT_HANDLERS, "subp", "clcp"]);
32
41
  /** Sample formats this can turn into cues. `stpp` (TTML) is XML and is not one. */
33
42
  const TEXT_FORMATS = new Set(["tx3g", "text", "wvtt"]);
34
43
 
@@ -218,6 +227,11 @@ function sampleOffsets(moov, stsc, chunkOffsets, sizes) {
218
227
  /**
219
228
  * @typedef {object} Mp4SubtitleTrack
220
229
  * @property {number} trackId
230
+ * @property {number} declaredIndex - Its position among ALL of the file's
231
+ * subtitle tracks, including the ones whose sample format this cannot turn
232
+ * into cues (`stpp` TTML). That is the number ffmpeg gives the same stream in
233
+ * `0:s:N`, which is the number the browser names; counting only the readable
234
+ * ones would shift every track after a TTML one.
221
235
  * @property {string} format - `tx3g`, `text` or `wvtt`.
222
236
  * @property {string} language - Three letters, as the file declares them.
223
237
  * @property {Mp4SubtitleSample[]} samples - In time order.
@@ -246,6 +260,9 @@ export async function readMp4SubtitlePlan(readRange, fileSize) {
246
260
 
247
261
  /** @type {Mp4SubtitleTrack[]} */
248
262
  const tracks = [];
263
+ // Counts every subtitle track the file has, whether or not this can read it,
264
+ // so the number handed out matches ffmpeg's `0:s:N`. See `declaredIndex`.
265
+ let declaredIndex = -1;
249
266
  for (const trak of childrenOf(moov, moovBox.dataOffset, moov.length, "trak")) {
250
267
  const mdia = childOf(moov, trak.dataOffset, trak.end, "mdia");
251
268
  if (!mdia) {
@@ -253,6 +270,13 @@ export async function readMp4SubtitlePlan(readRange, fileSize) {
253
270
  }
254
271
  const hdlr = childOf(moov, mdia.dataOffset, mdia.end, "hdlr");
255
272
  const handler = hdlr ? moov.toString("latin1", hdlr.dataOffset + 8, hdlr.dataOffset + 12) : "";
273
+ if (!SUBTITLE_HANDLERS.has(handler)) {
274
+ continue;
275
+ }
276
+ // Counted before the readability checks below, and before the handler is
277
+ // narrowed to the text ones: this number is the track's place in the file,
278
+ // not its place among the tracks this code can turn into cues.
279
+ declaredIndex += 1;
256
280
  if (!TEXT_HANDLERS.has(handler)) {
257
281
  continue;
258
282
  }
@@ -337,7 +361,7 @@ export async function readMp4SubtitlePlan(readRange, fileSize) {
337
361
  });
338
362
  }
339
363
  }
340
- tracks.push({ trackId, format, language, samples });
364
+ tracks.push({ trackId, declaredIndex, format, language, samples });
341
365
  }
342
366
  return { tracks };
343
367
  }