@torrent-tv/proxy 2.55.5 → 2.55.7

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,11 @@
1
+ ## 2.55.7
2
+
3
+ - **Fix**: The subtitle push subscription is now recorded under the torrent pool's own key, not the browser's registry key — the two are different strings whenever a source was added by a `.torrent` file (a `.torrent` and a magnet for the same film are different request bytes, hashed into different registry keys, but the SAME infohash) and were silently different in every other case too: a registry key is `sha1(sourceType:source)`, one per API session; the pool's key is `torrent:<infohash>`, deliberately shared across a magnet and a `.torrent` for the same content (item 10). The diagnostic logging added in 2.55.6 caught it directly, field case 2026-08-22: cues were found and logged repeatedly, and every push answered `found no subscribed channel` — the subscription and the publish had never been able to agree on a key, for any torrent, since the push feature shipped in 2.55.5. `data-channel-handler.js` now resolves the browser's registry key through `sourceRegistry` to `(sourceType, source)` and runs it through the same `deriveSourceKey` the pool itself uses, at the one point both keys are in hand — the subscribe intercept, before the request is even forwarded.
4
+
5
+ ## 2.55.6
6
+
7
+ - **Chore**: Every step of the subtitle push chain now logs on success, not only on failure. Field report 2026-08-22, playing `Minions.and.Monsters.1080p.mkv`: a track was switched on over a minute after the seed fetch found nothing (`bytes=7`, an empty `WEBVTT` — expected, the torrent had barely started), and no cues appeared. The proxy log carried no evidence either way — `warmActiveFiles` posted `Event.SUBTITLE_CUES_READY` silently, `publishSubtitleCues` sent (or found no subscriber for) a push silently, and `subscribeSubtitles` registered a channel silently. Confirmed separately by reading WebTorrent's own source that `verified` fires on every live piece completion (`_markVerified` inside `store.put`'s callback in `torrent.js`, not only at startup), so the event source itself is real; what could not be told apart without these lines is subscription, discovery, and delivery. Logs now name each: `subtitle push: channel subscribed to …`, `… cue(s) found, posting to main thread`, `… sent N cue(s) … to M/T channel(s)` (or `found no subscribed channel`).
8
+
1
9
  ## 2.55.5
2
10
 
3
11
  - **New**: Subtitle cues are now PUSHED to the browser the moment they are read, over the WebRTC data channel — not fetched by the browser on a timer. Every declared track was already being warmed off the piece-`verified` event (2.55.4); what changed is that the result now travels to the browser unprompted instead of sitting on the proxy until the next poll asked for it. `data-channel-handler.js` remembers which channel last asked about a file's subtitles (piggy-backing on the browser's own first `/api/subtitles?trackIndex=` request — no separate subscribe message) and sends new cues there directly (`{ type: "subtitle-cues", fileIndex, trackIndex, cues, language }`), for every track the container declares, not only the one on screen. Rides the existing `proxy-control` data channel — the same one the request itself used, which is never the one carrying segment bytes, so a push cannot queue behind video. `finalizeCues` (end-time synthesis + ASS-dialogue stripping) is factored out of the HTTP route into `services/torrent-worker/subtitle-cues.js` so a pushed cue and a pulled one are built the same way. A browser's one-off seed fetch per track (for whatever is already read at the moment a file opens) and the external-subtitle-FILE path (`.srt`/`.ass` beside the video — a single whole-file read, no incremental delivery to begin with) are unchanged.
package/bin/cli.js CHANGED
@@ -300,6 +300,7 @@ try {
300
300
  });
301
301
  app = started.app;
302
302
  actualPort = started.port;
303
+ const sourceRegistry = started.sourceRegistry;
303
304
  const directBaseUrl = explicitBaseUrl || `http://${bindHost}:${actualPort}`;
304
305
 
305
306
  // A native fault writes the whole address space out — 4.18 GB each on the
@@ -421,6 +422,10 @@ try {
421
422
  dataChannelHandler = createDataChannelHandler({
422
423
  proxyPort: actualPort,
423
424
  onLog: (message) => logger.info(message),
425
+ // Resolves a browser's registry sourceKey to the torrent pool's own key
426
+ // (the content's infohash) so the subtitle push subscription and the
427
+ // pool's own publish agree on what a source is called. See server.js.
428
+ sourceRegistry,
424
429
  // Lets a stuck send queue ask the transport what it is doing. Late-bound:
425
430
  // the manager is created below, with this handler already in hand.
426
431
  getTransportSnapshot: (sessionId) => webRtcManager?.getTransportSnapshot(sessionId) ?? null
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.55.5",
3
+ "version": "2.55.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
package/server.js CHANGED
@@ -290,6 +290,14 @@ export async function startProxyServer({
290
290
  await app.listen({ host, port: selectedPort });
291
291
  return {
292
292
  app,
293
- port: selectedPort
293
+ port: selectedPort,
294
+ // The browser only ever knows a source by its REGISTRY key (a hash of the
295
+ // raw request bytes, scoped to one API session) — never the torrent
296
+ // pool's own key (the content's infohash, shared across a magnet and a
297
+ // `.torrent` naming the same film). The subtitle push subscription is
298
+ // recorded from a browser request and published from the pool's side, so
299
+ // resolving one into the other is what lets the two ends agree on what
300
+ // they are both calling "sourceKey".
301
+ sourceRegistry
294
302
  };
295
303
  }
@@ -43,6 +43,8 @@
43
43
 
44
44
  /** @import { DataChannel } from 'node-datachannel' */
45
45
 
46
+ import { deriveSourceKey } from "./torrent-source-key.js";
47
+
46
48
  /**
47
49
  * Configuration for the data channel handler.
48
50
  *
@@ -299,7 +301,7 @@ export function encodeFrame(idBytes, bytes, done) {
299
301
  return frame;
300
302
  }
301
303
 
302
- export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapshot }) {
304
+ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapshot, sourceRegistry }) {
303
305
  /**
304
306
  * Channels currently interested in one file's subtitle cues, keyed by
305
307
  * `sourceKey:fileIndex`. Populated the moment a browser asks for an
@@ -324,7 +326,11 @@ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapsho
324
326
  set = new Set();
325
327
  subtitleSubscribers.set(key, set);
326
328
  }
329
+ const isNew = !set.has(channel);
327
330
  set.add(channel);
331
+ if (isNew) {
332
+ log(`[dc] subtitle push: channel subscribed to ${key} (${set.size} channel(s) now)`);
333
+ }
328
334
  }
329
335
 
330
336
  /** @param {DataChannel} channel */
@@ -346,18 +352,29 @@ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapsho
346
352
  function publishSubtitleCues({ sourceKey, fileIndex, trackIndex, cues, language }) {
347
353
  const set = subtitleSubscribers.get(`${sourceKey}:${fileIndex}`);
348
354
  if (!set || set.size === 0) {
355
+ log(
356
+ `[dc] subtitle push: ${cues.length} cue(s) for ${sourceKey.slice(0, 8)}:${fileIndex} track ${trackIndex} ` +
357
+ "found no subscribed channel"
358
+ );
349
359
  return;
350
360
  }
351
361
  const message = { type: "subtitle-cues", fileIndex, trackIndex, cues, language };
362
+ const total = set.size;
363
+ let sent = 0;
352
364
  for (const channel of set) {
353
365
  try {
354
366
  channel.sendMessage(JSON.stringify(message));
367
+ sent += 1;
355
368
  } catch {
356
369
  // Closed between the subscription and this send; onClosed will not
357
370
  // fire for a channel that is already gone, so drop it here too.
358
371
  set.delete(channel);
359
372
  }
360
373
  }
374
+ log(
375
+ `[dc] subtitle push: sent ${cues.length} cue(s) for ${sourceKey.slice(0, 8)}:${fileIndex} track ${trackIndex} ` +
376
+ `to ${sent}/${total} channel(s)`
377
+ );
361
378
  }
362
379
 
363
380
  /** Request id → its ASCII bytes; see {@link requestIdBytes}. */
@@ -564,13 +581,34 @@ export function createDataChannelHandler({ proxyPort, onLog, getTransportSnapsho
564
581
  // this walks incrementally). `fileIndex` alone would also scope this to
565
582
  // the wrong grain for the real case — a torrent can carry several playable
566
583
  // files — so the pair is what a push is ever addressed to.
584
+ //
585
+ // The browser's `sourceKey` is a REGISTRY key — a hash of the raw request
586
+ // bytes, one per (magnet-or-.torrent, this API session). The torrent pool
587
+ // publishes under its OWN key — the content's infohash, deliberately the
588
+ // SAME for a magnet and a `.torrent` naming the same film, so the two
589
+ // share one swarm (item 10). The two are different strings for the same
590
+ // torrent whenever a source was added by its `.torrent` file (a `.torrent`
591
+ // and a magnet are different request bytes, same infohash) — subscribing
592
+ // under the registry key found no publisher for that reason, not because
593
+ // nothing was ever read: field case 2026-08-22, cues were found and
594
+ // logged, every push answered "found no subscribed channel". Resolved to
595
+ // the pool's key here, the one place both are in hand.
567
596
  if (path === "/api/subtitles" && typeof query === "string") {
568
597
  const params = new URLSearchParams(query);
569
- const sourceKey = params.get("sourceKey");
598
+ const registrySourceKey = params.get("sourceKey");
570
599
  const fileIndex = Number(params.get("fileIndex"));
571
600
  const hasTrackIndex = params.get("trackIndex") !== null && params.get("trackIndex") !== "";
572
- if (sourceKey && Number.isInteger(fileIndex) && hasTrackIndex) {
573
- subscribeSubtitles(sourceKey, fileIndex, channel);
601
+ if (registrySourceKey && Number.isInteger(fileIndex) && hasTrackIndex) {
602
+ const record = sourceRegistry?.get(registrySourceKey);
603
+ if (record) {
604
+ try {
605
+ const poolSourceKey = await deriveSourceKey(record.sourceType, record.source);
606
+ subscribeSubtitles(poolSourceKey, fileIndex, channel);
607
+ } catch (error) {
608
+ log(`[dc] subtitle push: could not resolve ${registrySourceKey.slice(0, 8)} to a pool key: ` +
609
+ `${error instanceof Error ? error.message : error}`);
610
+ }
611
+ }
574
612
  }
575
613
  }
576
614
 
@@ -525,6 +525,10 @@ function warmActiveFiles(sourceKey, torrent) {
525
525
  warmSubtitleCues(torrent, fileIndex, sourceKey)
526
526
  .then((fresh) => {
527
527
  for (const entry of fresh) {
528
+ log(
529
+ `subtitle push ${sourceKey.slice(0, 8)}:${fileIndex} track ${entry.trackIndex}: ` +
530
+ `${entry.cues.length} new cue(s) found, posting to main thread`
531
+ );
528
532
  parentPort.postMessage({
529
533
  type: Event.SUBTITLE_CUES_READY,
530
534
  sourceKey,