@torrent-tv/proxy 2.101.25 → 2.101.27

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.101.25",
3
+ "version": "2.101.27",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "repository": {
@@ -75,10 +75,15 @@ export async function handleApiSourceFilesGet(req, reply, { sourceRegistry, torr
75
75
  if (consumerId) viewers?.visibleFiles(consumerId, sourceKey, contents.items.map(item => item.fileIndex));
76
76
  const generationRaw = typeof req.query?.generation === "string" ? req.query.generation : "";
77
77
  const generation = generationRaw !== "" && /^\d+$/.test(generationRaw) ? Number(generationRaw) : null;
78
+ const linkMbps = Number(req.query?.linkMbps);
79
+ const linkSampleAgeMs = Number(req.query?.linkSampleAgeMs);
80
+ const earlyLink = Number.isFinite(linkMbps) && linkMbps > 0
81
+ ? { linkMbps, linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null }
82
+ : {};
78
83
  const files = contents.files().map(file => {
79
84
  if (!consumerId || !Number.isSafeInteger(generation) || file.kind !== "video" || file.excludedReason) return file;
80
85
  const waitEstimate = viewerRequests?.waitEstimateForFile?.({ consumerId, generation, sourceKey,
81
- fileIndex: file.fileIndex, fileName: file.relativePath ?? file.name, fileLength: file.length }) ?? null;
86
+ fileIndex: file.fileIndex, fileName: file.relativePath ?? file.name, fileLength: file.length, ...earlyLink }) ?? null;
82
87
  return waitEstimate ? { ...file, waitEstimate } : file;
83
88
  });
84
89
  return reply.send({
@@ -37,9 +37,14 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
37
37
  const requestedFileName = typeof req.query.fileName === "string" ? req.query.fileName : "";
38
38
  const requestedFileLength = typeof req.query.fileLength === "string" && /^\d+$/.test(req.query.fileLength)
39
39
  ? Number(req.query.fileLength) : null;
40
+ const linkMbps = Number(req.query.linkMbps);
41
+ const linkSampleAgeMs = Number(req.query.linkSampleAgeMs);
42
+ const earlyLink = Number.isFinite(linkMbps) && linkMbps > 0
43
+ ? { linkMbps, linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null }
44
+ : {};
40
45
  const initialEstimate = () => consumerId && Number.isSafeInteger(generation) && Number.isSafeInteger(fileIndex)
41
46
  ? viewerRequests?.waitEstimateForFile?.({ consumerId, generation, sourceKey, fileIndex,
42
- fileName: requestedFileName, fileLength: requestedFileLength }) ?? null
47
+ fileName: requestedFileName, fileLength: requestedFileLength, ...earlyLink }) ?? null
43
48
  : null;
44
49
 
45
50
  let torrent;
@@ -137,7 +142,7 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
137
142
 
138
143
  const waitEstimate = consumerId && Number.isSafeInteger(generation)
139
144
  ? viewerRequests?.waitEstimateForSource?.({ consumerId, generation, sourceKey, fileIndex,
140
- fileName: torrent.files?.[fileIndex]?.name ?? requestedFileName, fileLength: requestedFileLength, stats }) ?? null
145
+ fileName: torrent.files?.[fileIndex]?.name ?? requestedFileName, fileLength: requestedFileLength, stats, ...earlyLink }) ?? null
141
146
  : null;
142
147
  return reply.send(waitEstimate ? { ...stats, waitEstimate } : stats);
143
148
  }
@@ -280,6 +280,8 @@ export class LocalObservations {
280
280
  fileLength: positive(features.fileLength),
281
281
  downloadSpeed: nonNegative(features.downloadSpeed),
282
282
  linkMbps: nonNegative(features.linkMbps),
283
+ linkSampleAgeMs: nonNegative(features.linkSampleAgeMs),
284
+ preparedOutputBytes: nonNegative(features.preparedOutputBytes),
283
285
  headerRemaining: nonNegative(features.headerRemaining),
284
286
  peers: nonNegative(features.peers),
285
287
  at: this.#now()
@@ -302,7 +304,8 @@ export class LocalObservations {
302
304
  const readings = this.#entries.firstFrame[this.#firstFrameKey()] ?? [];
303
305
  const candidates = readings.filter(reading => reading.extension === (features.extension ?? ""));
304
306
  if (candidates.length === 0) return null;
305
- const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "headerRemaining", "peers"];
307
+ const dimensions = ["fileLength", "downloadSpeed", "linkMbps", "linkSampleAgeMs", "preparedOutputBytes",
308
+ "headerRemaining", "peers"];
306
309
  const scales = new Map(dimensions.map(key => [key, median(candidates
307
310
  .map(reading => nonNegative(reading[key]))
308
311
  .filter(value => value !== null && value > 0))]));
@@ -106,7 +106,7 @@ export class ViewerRequests {
106
106
  * @param {{ consumerId: string, generation: number, sourceKey: string, fileIndex: number, fileName: string, fileLength: number }} params
107
107
  * @returns {object | null}
108
108
  */
109
- waitEstimateForFile({ consumerId, generation, sourceKey, fileIndex, fileName, fileLength }) {
109
+ waitEstimateForFile({ consumerId, generation, sourceKey, fileIndex, fileName, fileLength, linkMbps, linkSampleAgeMs }) {
110
110
  if (!consumerId || !Number.isSafeInteger(generation) || typeof sourceKey !== "string" ||
111
111
  !Number.isSafeInteger(fileIndex) || fileIndex < 0) return null;
112
112
  const waitId = `${sourceKey}:${fileIndex}:${generation}`;
@@ -116,7 +116,10 @@ export class ViewerRequests {
116
116
  extension,
117
117
  fileLength,
118
118
  downloadSpeed: null,
119
- linkMbps: this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
119
+ linkMbps: Number.isFinite(linkMbps) && linkMbps > 0
120
+ ? linkMbps : this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
121
+ linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
122
+ preparedOutputBytes: this.#preparedOutputBytes(sourceKey, fileIndex),
120
123
  headerRemaining: null,
121
124
  peers: null
122
125
  };
@@ -144,7 +147,8 @@ export class ViewerRequests {
144
147
  * @param {{ consumerId: string, generation: number, sourceKey: string, fileIndex: number | null, fileName?: string, fileLength?: number | null, stats?: object }} params
145
148
  * @returns {object | null}
146
149
  */
147
- waitEstimateForSource({ consumerId, generation, sourceKey, fileIndex, fileName = "", fileLength = null, stats = {} }) {
150
+ waitEstimateForSource({ consumerId, generation, sourceKey, fileIndex, fileName = "", fileLength = null,
151
+ stats = {}, linkMbps, linkSampleAgeMs }) {
148
152
  if (!consumerId || !Number.isSafeInteger(generation) || typeof sourceKey !== "string") return null;
149
153
  const waitId = `${sourceKey}:${fileIndex ?? "all"}:${generation}`;
150
154
  const key = `${consumerId}:${waitId}`;
@@ -154,7 +158,11 @@ export class ViewerRequests {
154
158
  fileLength: Number.isFinite(stats?.fileLength) && stats.fileLength > 0
155
159
  ? stats.fileLength : fileLength,
156
160
  downloadSpeed: stats?.downloadSpeed,
157
- linkMbps: this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
161
+ linkMbps: Number.isFinite(linkMbps) && linkMbps > 0
162
+ ? linkMbps : this.#host.viewers.get(consumerId)?.linkReading()?.linkMbps,
163
+ linkSampleAgeMs: Number.isFinite(linkSampleAgeMs) && linkSampleAgeMs >= 0 ? linkSampleAgeMs : null,
164
+ preparedOutputBytes: Number.isSafeInteger(fileIndex) && fileIndex >= 0
165
+ ? this.#preparedOutputBytes(sourceKey, fileIndex) : null,
158
166
  headerRemaining: Number.isFinite(stats?.headerBytes) && Number.isFinite(stats?.headerDownloadedBytes)
159
167
  ? Math.max(0, stats.headerBytes - stats.headerDownloadedBytes) : null,
160
168
  peers: stats?.connectedPeers ?? stats?.numPeers
@@ -216,6 +224,23 @@ export class ViewerRequests {
216
224
  return true;
217
225
  }
218
226
 
227
+ #preparedOutputBytes(sourceKey, fileIndex) {
228
+ const outputs = this.#host.outputs;
229
+ const segmentStore = this.#host.segmentStore;
230
+ if (!outputs?.values || !segmentStore?.sizesOf) return null;
231
+ let bytes = 0;
232
+ const seen = new Set();
233
+ for (const output of outputs.values()) {
234
+ if (output.file?.sourceKey !== sourceKey || output.file?.fileIndex !== fileIndex || !output.outputKey) continue;
235
+ if (seen.has(output.outputKey)) continue;
236
+ seen.add(output.outputKey);
237
+ for (const size of segmentStore.sizesOf(output.outputKey).values()) {
238
+ if (Number.isFinite(size) && size > 0) bytes += size;
239
+ }
240
+ }
241
+ return bytes;
242
+ }
243
+
219
244
  #pruneEarlyWaits() {
220
245
  const cutoff = Date.now() - 30 * 60_000;
221
246
  for (const [key, value] of this.#earlyWaits) {