@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 +8 -0
- package/bin/cli.js +5 -0
- package/package.json +1 -1
- package/server.js +9 -1
- package/services/data-channel-handler.js +42 -4
- package/services/torrent-worker/worker.js +4 -0
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
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
|
|
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 (
|
|
573
|
-
|
|
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,
|