@torrent-tv/proxy 2.97.28 → 2.97.29

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.97.28",
3
+ "version": "2.97.29",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -59,10 +59,19 @@ export class DownloadMaps {
59
59
  }
60
60
  const deadlineAt = Number.isFinite(current.deadlineAt)
61
61
  ? current.deadlineAt + (Math.max(0, zone.from - current.from) - (zone.leadSeconds ?? 0)) * 1000 : current.deadlineAt;
62
- if (Object.keys(current).some(key => key !== "from" && key !== "to" && !Object.is(zone[key], key === "deadlineAt" ? deadlineAt : current[key]))) changed = true;
62
+ // A deadline worked out again on a later clock is no change: the
63
+ // torrent orders by deadlines and is sent their order, which the pass
64
+ // below states with the rest.
65
+ if (Object.keys(current).some(key => key !== "from" && key !== "to" && key !== "deadlineAt" && key !== "withinSeconds" &&
66
+ !Object.is(zone[key], current[key]))) changed = true;
63
67
  return [{ ...zone, ...current, from: zone.from, to: zone.to, deadlineAt }];
64
68
  });
65
- this.#setZones(file, retained);
69
+ // THE ZONES STAND AS THEY ARE UNLESS ONE IS WITHDRAWN OR RE-WORDED. Set
70
+ // anew on every pass, they were a new list each time, so every metadata
71
+ // statement during the pass packed the whole film again: 24 s of the main
72
+ // thread in fifteen minutes of a two-hour AVI on Home Assistant (r24,
73
+ // torrent-tv/meta#178).
74
+ if (changed) this.#setZones(file, retained);
66
75
  for (const [statement, metadata] of file.metadata) {
67
76
  if (metadata.interval && !this.wantsInterval(sourceKey, fileIndex, metadata.interval)) {
68
77
  file.metadata.delete(statement);
@@ -19,22 +19,83 @@
19
19
  */
20
20
 
21
21
  /**
22
- * Bytes covered by a list of inclusive ranges, each byte counted once: the
23
- * declarations and indexes every segment of an original file names are held
24
- * once by the run that reads them.
25
- *
26
- * @param {Array<[number, number]>} ranges
27
- * @returns {number}
22
+ * The bytes a run's input holds, each counted once: the declarations and
23
+ * indexes every segment of an original file names are held once by the run
24
+ * that reads them. Kept merged and in order, so a segment's ranges are
25
+ * counted against it by search rather than by sorting everything held again —
26
+ * a soundtrack's segments add a few bytes each, and its stretch is walked far.
28
27
  */
29
- function coveredBytes(ranges) {
30
- let total = 0;
31
- let end = -1;
32
- for (const [start, last] of [...ranges].sort((left, right) => left[0] - right[0])) {
33
- if (last <= end) continue;
34
- total += last - Math.max(start, end + 1) + 1;
35
- end = last;
28
+ class HeldBytes {
29
+ /** Inclusive [start, end], merged, ascending. @type {Array<[number, number]>} */
30
+ #ranges = [];
31
+ total = 0;
32
+
33
+ /** @param {Array<[number, number]>} ranges */
34
+ constructor(ranges = []) {
35
+ this.add(ranges);
36
+ }
37
+
38
+ /** The first held range that ends at or after `at`. */
39
+ #from(at) {
40
+ let low = 0;
41
+ let high = this.#ranges.length;
42
+ while (low < high) {
43
+ const middle = (low + high) >> 1;
44
+ if (this.#ranges[middle][1] < at) low = middle + 1;
45
+ else high = middle;
46
+ }
47
+ return low;
48
+ }
49
+
50
+ /**
51
+ * What these ranges would add, without adding them.
52
+ *
53
+ * @param {Array<[number, number]>} ranges
54
+ * @returns {number}
55
+ */
56
+ adds(ranges) {
57
+ let added = 0;
58
+ for (const [start, end] of merged(ranges)) {
59
+ let at = start;
60
+ for (let index = this.#from(start); at <= end && index < this.#ranges.length && this.#ranges[index][0] <= end; index++) {
61
+ const [heldStart, heldEnd] = this.#ranges[index];
62
+ if (heldStart > at) added += heldStart - at;
63
+ at = Math.max(at, heldEnd + 1);
64
+ }
65
+ if (at <= end) added += end - at + 1;
66
+ }
67
+ return added;
68
+ }
69
+
70
+ /** @param {Array<[number, number]>} ranges */
71
+ add(ranges) {
72
+ for (const [start, end] of merged(ranges)) {
73
+ this.total += this.adds([[start, end]]);
74
+ // Joined with every held range it touches or overlaps.
75
+ const first = this.#from(start - 1);
76
+ let last = first;
77
+ let joinedStart = start;
78
+ let joinedEnd = end;
79
+ while (last < this.#ranges.length && this.#ranges[last][0] <= end + 1) {
80
+ joinedStart = Math.min(joinedStart, this.#ranges[last][0]);
81
+ joinedEnd = Math.max(joinedEnd, this.#ranges[last][1]);
82
+ last++;
83
+ }
84
+ this.#ranges.splice(first, last - first, [joinedStart, joinedEnd]);
85
+ }
36
86
  }
37
- return total;
87
+ }
88
+
89
+ /** Inclusive ranges merged where they overlap or touch, ascending. @param {Array<[number, number]>} ranges */
90
+ function merged(ranges) {
91
+ const sorted = [...ranges].sort((left, right) => left[0] - right[0]);
92
+ const out = [];
93
+ for (const [start, end] of sorted) {
94
+ const previous = out.at(-1);
95
+ if (previous && start <= previous[1] + 1) previous[1] = Math.max(previous[1], end);
96
+ else out.push([start, end]);
97
+ }
98
+ return out;
38
99
  }
39
100
 
40
101
  /**
@@ -48,39 +109,56 @@ function coveredBytes(ranges) {
48
109
  * @returns {Zone[]} In their order; raised zones are copies.
49
110
  */
50
111
  export function expandToTargetInputs(zones, targetBytesOf) {
51
- /** @type {Map<string, Map<number, Zone[]>>} */
52
- const byOutput = new Map();
112
+ // ONE WALK FOR EACH OUTPUT'S FIRST SEGMENT, and the segments after it only
113
+ // as far as the run's input reaches. The map names every segment of a film,
114
+ // and each pass used to group all of them and work out each one's highest
115
+ // priority before looking at the few the run copies: 26 s of the main thread
116
+ // in fifteen minutes of a two-hour AVI on Home Assistant (r24,
117
+ // torrent-tv/meta#178).
118
+ /** @type {Map<string, { index: number, priority: number, zone: Zone }>} */
119
+ const first = new Map();
53
120
  for (const zone of zones) {
54
- let segments = byOutput.get(zone.outputKey);
55
- if (!segments) byOutput.set(zone.outputKey, segments = new Map());
56
- const list = segments.get(zone.index);
57
- if (list) list.push(zone);
58
- else segments.set(zone.index, [zone]);
121
+ const best = first.get(zone.outputKey);
122
+ if (!best || zone.priority > best.priority || (zone.priority === best.priority && zone.index < best.index)) {
123
+ first.set(zone.outputKey, { index: zone.index, priority: zone.priority, zone });
124
+ }
59
125
  }
60
- /** @type {Map<Zone, Zone>} */
61
- const raised = new Map();
62
- for (const [outputKey, segments] of byOutput) {
126
+ /** @type {Map<string, { first: { index: number, priority: number, zone: Zone }, target: number, after: Map<number, Zone[]> }>} */
127
+ const runs = new Map();
128
+ for (const [outputKey, best] of first) {
63
129
  const target = targetBytesOf(outputKey);
64
- if (!(target > 0)) continue;
65
- let first = null;
66
- for (const [index, list] of segments) {
67
- const priority = Math.max(...list.map(zone => zone.priority));
68
- if (first === null || priority > first.priority || (priority === first.priority && index < first.index)) {
69
- first = { index, priority, zone: list.find(zone => zone.priority === priority) };
70
- }
130
+ if (target > 0) runs.set(outputKey, { first: best, target, after: new Map() });
131
+ }
132
+ if (runs.size === 0) return zones;
133
+ /** @type {Map<string, Array<[number, number]>>} */
134
+ const held = new Map();
135
+ for (const zone of zones) {
136
+ const run = runs.get(zone.outputKey);
137
+ if (!run) continue;
138
+ if (zone.index === run.first.index) {
139
+ let ranges = held.get(zone.outputKey);
140
+ if (!ranges) held.set(zone.outputKey, ranges = []);
141
+ ranges.push([zone.byteStart, zone.byteEnd]);
142
+ } else if (zone.index > run.first.index) {
143
+ const list = run.after.get(zone.index);
144
+ if (list) list.push(zone);
145
+ else run.after.set(zone.index, [zone]);
71
146
  }
72
- if (first === null) continue;
73
- const held = segments.get(first.index).map(zone => [zone.byteStart, zone.byteEnd]);
147
+ }
148
+ /** @type {Map<Zone, Zone>} */
149
+ const raised = new Map();
150
+ for (const [outputKey, run] of runs) {
151
+ const holding = new HeldBytes(held.get(outputKey) ?? []);
74
152
  // The run's input is a stretch of consecutive segments: one already made,
75
153
  // or not yet worked out, ends it.
76
- for (let index = first.index + 1; segments.has(index); index++) {
77
- const list = segments.get(index);
78
- const ranges = [...held, ...list.map(zone => [zone.byteStart, zone.byteEnd])];
79
- if (coveredBytes(ranges) > target) break;
80
- held.push(...list.map(zone => [zone.byteStart, zone.byteEnd]));
154
+ for (let index = run.first.index + 1; run.after.has(index); index++) {
155
+ const list = run.after.get(index);
156
+ const ranges = list.map(zone => [zone.byteStart, zone.byteEnd]);
157
+ if (holding.total + holding.adds(ranges) > run.target) break;
158
+ holding.add(ranges);
81
159
  for (const zone of list) {
82
- raised.set(zone, { ...zone, priority: first.priority, urgent: first.zone.urgent,
83
- behind: first.zone.behind, deferred: first.zone.deferred });
160
+ raised.set(zone, { ...zone, priority: run.first.priority, urgent: run.first.zone.urgent,
161
+ behind: run.first.zone.behind, deferred: run.first.zone.deferred });
84
162
  }
85
163
  }
86
164
  }