@torrent-tv/proxy 2.105.5 → 2.105.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.105.5",
3
+ "version": "2.105.7",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
package/server.js CHANGED
@@ -754,84 +754,140 @@ export async function startProxyServer({
754
754
  if (!map.isCurrent()) return [];
755
755
  await turn();
756
756
  if (!map.isCurrent()) return [];
757
- const reader = readers.get(zone.outputKey);
758
- if (!reader) continue;
757
+ const resolutionStartedAt = performance.now();
758
+ const resolution = { outcome: "unresolved", method: null, trackCount: null, rangeCount: null, rangeBytes: null };
759
+ const resolutionId = `${map.fileIndex}:${zone.outputKey}:${zone.index}:${passStartedAt.toFixed(3)}`;
760
+ waitTrace.source(map.sourceKey, "process", { process: "P4.1", phase: "begin", resolutionId,
761
+ sourceId: `${map.sourceKey}:${map.fileIndex}`, fileIndex: map.fileIndex, outputKey: zone.outputKey,
762
+ segment: zone.index, priority: zone.priority, deadlineAt: zone.deadlineAt });
763
+ try {
764
+ const reader = readers.get(zone.outputKey);
765
+ if (!reader) {
766
+ resolution.outcome = "reader-missing";
767
+ continue;
768
+ }
759
769
  // WHAT A SEGMENT READS IS WORKED OUT ONCE (`segmentRecordOf`). Rebuilt
760
770
  // for every segment of the film on every map, this pass held the main
761
771
  // thread up to 1.5 s at a time on Home Assistant (torrent-tv/meta#178).
762
- const record = segmentRecordOf(reader.records, { index: zone.index, tracks: reader.choices }, reader.facts);
763
- const reading = record.reading;
764
- if (!reading) {
765
- projection.setUnreadable(zone.outputKey, zone.index);
766
- continue;
767
- }
768
- const { wanted, modes, interval, rangesKey } = reading;
769
- const convert = ranges => {
770
- if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
771
- record.ranges = ranges;
772
- record.pieceLength = pieceLength;
773
- record.joined = joinedWithinPieces(ranges, pieceLength);
774
- }
775
- const joined = record.joined;
776
- // What the segment reads is kept for the forecast in a table of its
777
- // own, whatever form the torrent is told it in.
778
- if (record.registered !== joined) {
779
- segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
780
- record.registered = joined;
772
+ const record = segmentRecordOf(reader.records, { index: zone.index, tracks: reader.choices }, reader.facts);
773
+ const reading = record.reading;
774
+ if (!reading) {
775
+ resolution.outcome = "segment-reading-unavailable";
776
+ projection.setUnreadable(zone.outputKey, zone.index);
777
+ continue;
781
778
  }
782
- projection.setRanges(zone.outputKey, zone.index, joined);
783
- resolved += 1;
784
- };
779
+ const { wanted, modes, interval, rangesKey } = reading;
780
+ resolution.trackCount = wanted.length;
781
+ resolution.trackIds = wanted.map(track => track.trackNumber);
782
+ resolution.sourceInterval = { from: interval.from, to: interval.to };
783
+ const convert = (ranges, method) => {
784
+ if (record.ranges !== ranges || record.pieceLength !== pieceLength) {
785
+ record.ranges = ranges;
786
+ record.pieceLength = pieceLength;
787
+ record.joined = joinedWithinPieces(ranges, pieceLength);
788
+ }
789
+ const joined = record.joined;
790
+ resolution.method = method;
791
+ resolution.rangeCount = joined.length;
792
+ resolution.rangeBytes = joined.reduce((total, [start, end]) => total + end - start + 1, 0);
793
+ // What the segment reads is kept for the forecast in a table of its
794
+ // own, whatever form the torrent is told it in.
795
+ if (record.registered !== joined) {
796
+ segmentInputs.set(map.sourceKey, map.fileIndex, zone.outputKey, zone.index, joined);
797
+ record.registered = joined;
798
+ }
799
+ projection.setRanges(zone.outputKey, zone.index, joined);
800
+ resolution.outcome = "resolved";
801
+ resolved += 1;
802
+ };
785
803
  // The bytes one segment needs are a fact of the file and the output, so
786
804
  // they are worked out once. A whole film is thousands of segments and
787
805
  // the map changes every few seconds; worked out afresh on every change,
788
806
  // a pass over a two-hour AVI never finished and the next segment's bytes
789
807
  // were never asked for (Home Assistant 2026-10-08, torrent-tv/meta#151).
790
- const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
791
- if (known) { convert(known); continue; }
792
- if (waiting.has(zone.outputKey)) continue;
808
+ const known = record.ranges ?? segmentSourceRanges.get(rangesKey);
809
+ if (known) { convert(known, record.ranges ? "segment-record-cache" : "source-range-cache"); continue; }
810
+ if (waiting.has(zone.outputKey)) {
811
+ resolution.outcome = "waiting-for-output-input";
812
+ continue;
813
+ }
793
814
  // What is resolved so far is stated before a lower zone is worked out
794
815
  // (DownloadMaps, torrent-tv/meta#178).
795
- map.workingOut?.(zone.priority, () => projection.windows());
796
- if (direct) {
816
+ map.workingOut?.(zone.priority, () => projection.windows());
817
+ if (direct) {
797
818
  // Asked per zone too: a soundtrack of an AVI is read from its own
798
819
  // packets, which the same index states (torrent-tv/meta#166).
799
- let value = null;
800
- try {
801
- value = await directSegmentInput(direct, { interval, tracks: wanted, mode: track => modes.get(track) },
802
- built => refreshWhenBuilt(direct, built, map));
803
- } catch { value = null; }
820
+ let value = null;
821
+ try {
822
+ value = await directSegmentInput(direct, { interval, tracks: wanted, mode: track => modes.get(track) },
823
+ built => refreshWhenBuilt(direct, built, map));
824
+ } catch { value = null; }
804
825
  // A table still being built holds this output's segments, and only
805
826
  // this output's: the map is built again when it is.
806
- if (value?.kind === "pending") {
807
- waiting.add(zone.outputKey);
827
+ if (value?.kind === "pending") {
828
+ resolution.method = "direct-source-index";
829
+ resolution.outcome = "source-index-pending";
830
+ waiting.add(zone.outputKey);
831
+ continue;
832
+ }
833
+ if (value?.kind === "result") segmentSourceRanges.set(rangesKey, value.ranges);
834
+ if (value?.kind === "result") {
835
+ convert(value.ranges, "direct-source-index");
836
+ continue;
837
+ }
838
+ }
839
+ resolution.method = "container-index-fallback";
840
+ const intervalParams = await containerOver({ ...map, packetInterval: interval,
841
+ requestId: `download:${map.sourceKey}:${map.fileIndex}:${zone.outputKey}:${zone.index}`,
842
+ demand: { ...zone.owner, leadSeconds: zone.leadSeconds } });
843
+ if (!map.isCurrent()) {
844
+ resolution.outcome = "map-replaced";
845
+ return [];
846
+ }
847
+ if (!intervalParams) {
848
+ resolution.outcome = "source-file-unavailable";
808
849
  continue;
809
850
  }
810
- if (value?.kind === "result") segmentSourceRanges.set(rangesKey, value.ranges);
811
- if (value?.kind === "result") {
812
- convert(value.ranges);
851
+ const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
852
+ if (!map.isCurrent()) {
853
+ resolution.outcome = "map-replaced";
854
+ return [];
855
+ }
856
+ if (navigation.kind !== "result") {
857
+ resolution.outcome = `source-navigation-${navigation.kind}`;
858
+ if (navigation.kind === "needs-ranges" || navigation.kind === "needs-memory") waiting.add(zone.outputKey);
813
859
  continue;
814
860
  }
861
+ const originalRanges = navigation.value;
862
+ const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
863
+ if (!map.isCurrent()) {
864
+ resolution.outcome = "map-replaced";
865
+ return [];
866
+ }
867
+ if (packets.kind === "needs-memory") waiting.add(zone.outputKey);
868
+ if (packets.kind !== "result") {
869
+ resolution.outcome = `source-packets-${packets.kind}`;
870
+ continue;
871
+ }
872
+ const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
873
+ if (input.kind !== "result") {
874
+ resolution.outcome = `segment-input-${input.kind}`;
875
+ continue;
876
+ }
877
+ segmentSourceRanges.set(rangesKey, input.ranges);
878
+ convert(input.ranges, originalRanges ? "container-source-ranges" : "packet-index");
879
+ } catch (error) {
880
+ resolution.outcome = "error";
881
+ resolution.error = error?.message ?? String(error);
882
+ throw error;
883
+ } finally {
884
+ const resolutionEndedAt = performance.now();
885
+ waitTrace.source(map.sourceKey, "process", { process: "P4.1", phase: "end", resolutionId,
886
+ sourceId: `${map.sourceKey}:${map.fileIndex}`, fileIndex: map.fileIndex, outputKey: zone.outputKey,
887
+ segment: zone.index, priority: zone.priority,
888
+ startedAt: resolutionStartedAt, endedAt: resolutionEndedAt, durationMs: resolutionEndedAt - resolutionStartedAt,
889
+ ...resolution });
815
890
  }
816
- const intervalParams = await containerOver({ ...map, packetInterval: interval,
817
- requestId: `download:${map.sourceKey}:${map.fileIndex}:${zone.outputKey}:${zone.index}`,
818
- demand: { ...zone.owner, leadSeconds: zone.leadSeconds } });
819
- if (!map.isCurrent()) return [];
820
- if (!intervalParams) continue;
821
- const navigation = await containerOrchestrator.inspect(intervalParams, "source-navigation");
822
- if (navigation.kind !== "result") {
823
- if (navigation.kind === "needs-ranges" || navigation.kind === "needs-memory") waiting.add(zone.outputKey);
824
- continue;
825
- }
826
- const originalRanges = navigation.value;
827
- const packets = await containerOrchestrator.inspect(intervalParams, originalRanges ? "source-ranges" : "packets");
828
- if (!map.isCurrent()) return [];
829
- if (packets.kind === "needs-memory") waiting.add(zone.outputKey);
830
- if (packets.kind !== "result") continue;
831
- const input = originalRanges ? packets.value : new SegmentInputs({ index: packets.value, tracks: wanted }).forInterval({ ...interval, mode: track => modes.get(track) });
832
- if (input.kind !== "result") continue;
833
- segmentSourceRanges.set(rangesKey, input.ranges);
834
- convert(input.ranges);
835
891
  }
836
892
  const result = projection.windows();
837
893
  notePass({ whole: reworked.whole, reworked: reworked.segments, resolved, windows: result.length,
@@ -76,7 +76,14 @@ export class SubtitleOrchestrator {
76
76
  endSeconds: packet.duration > 0 ? packet.pts + packet.duration : null, text };
77
77
  pending.push({ identity, cue });
78
78
  }
79
- if (params.isCurrent?.() === false || this.#packetCues.get(key) !== state) {
79
+ // A cue read from the file is a fact about the file, not about where
80
+ // the viewer stood when the read began. A position report moves that
81
+ // stand every 1.5 s, so a read that crossed one used to throw away the
82
+ // cues it had read, and those lines never reached the page
83
+ // (torrent-tv/meta#7). They are kept and published; only a forgotten
84
+ // source cannot take them.
85
+ const obsolete = params.isCurrent?.() === false;
86
+ if (this.#packetCues.get(key) !== state) {
80
87
  return { kind: "terminal", reason: "request-obsolete", requestId };
81
88
  }
82
89
  state.ready = true;
@@ -96,7 +103,7 @@ export class SubtitleOrchestrator {
96
103
  }
97
104
  }
98
105
  const fresh = TextSubtitleTrack.finalizeCues(ordered(), track.codecId).filter(cue => !previous.has(cue.seq));
99
- if (fresh.length && params.isCurrent?.() !== false) {
106
+ if (fresh.length) {
100
107
  const cues = fresh;
101
108
  this.cues.publish?.({ sourceKey: params.sourceKey, fileIndex: params.fileIndex,
102
109
  trackIndex: track.declaredIndex, cues, withdrawn, language: track.language ?? "",
@@ -104,6 +111,7 @@ export class SubtitleOrchestrator {
104
111
  cursor: state.cursor, spanStartSeconds: cues[0]?.startSeconds ?? null,
105
112
  spanEndSeconds: cues.at(-1)?.endSeconds ?? null, walkedClusters: 0, indexedClusters: 0 });
106
113
  }
114
+ if (obsolete) return { kind: "terminal", reason: "request-obsolete", requestId };
107
115
  result = { kind: "result", value: { cues: [...state.packets.values()], track, coveredClusters: 0, indexedClusters: 0 }, requestId };
108
116
  }
109
117
  } catch (error) {