@torrent-tv/proxy 2.89.14 → 2.89.15
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,7 @@
|
|
|
1
|
+
## 2.89.15
|
|
2
|
+
|
|
3
|
+
- **Fix**: A torrent downloaded whole is no longer removed while something is still reading it. The sweep that writes whole files out and removes the torrent asked only who had stated a need, and a read whose pieces are all here waits for nothing and states nothing. On the stand on 2026-10-04 the torrent was removed under ffmpeg's own read of the film: the read ended with "Piece store is closed" 277 MB in, ffmpeg reported `Stream ends prematurely`, the copied picture lost 2.6 s, and the page refused to start with "Prepared media contains a timestamp gap" (torrent-tv/meta#105). Open reads now count as use.
|
|
4
|
+
|
|
1
5
|
## 2.89.14
|
|
2
6
|
|
|
3
7
|
- **Fix**: A failed run counts only the source's own streams. The planner probes with an output, so ffmpeg's banner goes on to list the streams it maps and writes, and those were counted too: a file with one picture, one soundtrack, one subtitle and one font read as `2 video, 2 audio, 1 subtitle, 3 other` in the field — enough to make a second soundtrack look present when it is not.
|
package/package.json
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which files of a torrent somebody is using, for the sweep that writes
|
|
3
|
+
* whole files out and removes a torrent that has nothing left to fetch.
|
|
4
|
+
*
|
|
5
|
+
* Two facts, and both are needed. A file somebody has stated a need for is in
|
|
6
|
+
* use. So is a file being read: a read whose pieces are all here waits for
|
|
7
|
+
* nothing, and so states nothing in the demand register. Field 2026-10-04: a
|
|
8
|
+
* torrent was removed under such a read, ffmpeg's input ended with "Piece store
|
|
9
|
+
* is closed" 277 MB in, and the copied picture lost 2.6 s that the readiness
|
|
10
|
+
* forecast then refused as a timestamp gap (torrent-tv/meta#105).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Claimants that keep pieces coming without anybody reading. */
|
|
14
|
+
const NOT_A_READER = ["file-edges:", "torrent-fill:", "background-fill:"];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @param {object} options
|
|
18
|
+
* @param {object} options.torrent
|
|
19
|
+
* @param {Array<{ claimant: unknown, fileIndex: number }>} options.windows - The demand register's windows.
|
|
20
|
+
* @param {Iterable<{ torrent: object, fileIndex: number }>} options.openReads - Reads in flight.
|
|
21
|
+
* @returns {Set<number>} File indices in use.
|
|
22
|
+
*/
|
|
23
|
+
export function filesInUse({ torrent, windows, openReads }) {
|
|
24
|
+
const used = new Set(
|
|
25
|
+
windows
|
|
26
|
+
.filter((window) => !NOT_A_READER.some((prefix) => String(window.claimant).startsWith(prefix)))
|
|
27
|
+
.map((window) => window.fileIndex)
|
|
28
|
+
);
|
|
29
|
+
for (const read of openReads) {
|
|
30
|
+
if (read.torrent === torrent) {
|
|
31
|
+
used.add(read.fileIndex);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return used;
|
|
35
|
+
}
|
|
@@ -34,6 +34,7 @@ import { heldRangesOf, ownsItsMemory, readHeldBytes } from "./held-bytes.js";
|
|
|
34
34
|
import { CompletedFiles, completedFilesRoot } from "../../storage/files/CompletedFiles.js";
|
|
35
35
|
import { pieceFromWholeFiles, pieceIsInWholeFiles } from "../../storage/files/piece-from-whole-file.js";
|
|
36
36
|
import { Command, Event } from "./protocol.js";
|
|
37
|
+
import { filesInUse } from "./files-in-use.js";
|
|
37
38
|
import { startMemoryReport, WORKER_MEMORY_SAMPLE_MS } from "../../storage/memory-report.js";
|
|
38
39
|
import { forwardLogsTo, logger } from "../../../utils/logger.js";
|
|
39
40
|
|
|
@@ -94,6 +95,14 @@ const torrentsByKey = new Map();
|
|
|
94
95
|
const sourceRecipes = new Map();
|
|
95
96
|
/** In-flight reads, so a cancel can stop one mid-body. */
|
|
96
97
|
const readsById = new Map();
|
|
98
|
+
/**
|
|
99
|
+
* What each in-flight read is reading. A read whose pieces are all here waits
|
|
100
|
+
* for nothing and so states nothing in the demand register; this is how the
|
|
101
|
+
* thread knows it is still open.
|
|
102
|
+
*
|
|
103
|
+
* @type {Map<number, { torrent: object, fileIndex: number }>}
|
|
104
|
+
*/
|
|
105
|
+
const openReads = new Map();
|
|
97
106
|
|
|
98
107
|
/**
|
|
99
108
|
* Shorthand for this file. The same path as `logger.info` anywhere else in the
|
|
@@ -257,6 +266,7 @@ async function streamRange({ id, sourceKey, fileIndex, start, end, windowBytes }
|
|
|
257
266
|
|
|
258
267
|
const sender = createSendStream({ port: parentPort, requestId: id });
|
|
259
268
|
readsById.set(id, sender);
|
|
269
|
+
openReads.set(id, { torrent, fileIndex });
|
|
260
270
|
|
|
261
271
|
const rangeStart = start ?? 0;
|
|
262
272
|
const rangeEnd = end ?? file.length - 1;
|
|
@@ -295,6 +305,7 @@ async function streamRange({ id, sourceKey, fileIndex, start, end, windowBytes }
|
|
|
295
305
|
throw error;
|
|
296
306
|
} finally {
|
|
297
307
|
readsById.delete(id);
|
|
308
|
+
openReads.delete(id);
|
|
298
309
|
// Any fragment still awaiting confirmation will never get one now; settling
|
|
299
310
|
// it here releases its pin rather than leaking a held slot.
|
|
300
311
|
settleFragment(id);
|
|
@@ -947,18 +958,12 @@ async function keepWholeFiles() {
|
|
|
947
958
|
if (!infoHash || !Array.isArray(torrent.files)) {
|
|
948
959
|
continue;
|
|
949
960
|
}
|
|
950
|
-
// What anybody wants of this torrent
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
!String(window.claimant).startsWith("file-edges:") &&
|
|
957
|
-
!String(window.claimant).startsWith("torrent-fill:") &&
|
|
958
|
-
!String(window.claimant).startsWith("background-fill:")
|
|
959
|
-
)
|
|
960
|
-
.map((window) => window.fileIndex)
|
|
961
|
-
);
|
|
961
|
+
// What anybody wants of this torrent or is reading from it now.
|
|
962
|
+
const wanted = filesInUse({
|
|
963
|
+
torrent,
|
|
964
|
+
windows: demandFor(torrent).register.windows(),
|
|
965
|
+
openReads: openReads.values()
|
|
966
|
+
});
|
|
962
967
|
for (const [fileIndex, file] of torrent.files.entries()) {
|
|
963
968
|
const key = `${infoHash}/${fileIndex}`;
|
|
964
969
|
if (file?.done !== true || completedFiles.find(infoHash, fileIndex) || beingKept.has(key)) {
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which files of a torrent are in use for the whole-file sweep. Pure:
|
|
3
|
+
* no torrent is started (see services/torrent/worker/files-in-use.js).
|
|
4
|
+
*/
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
import assert from "node:assert/strict";
|
|
7
|
+
import { filesInUse } from "../services/torrent/worker/files-in-use.js";
|
|
8
|
+
|
|
9
|
+
test("a file being read is in use though nobody states a need for it", () => {
|
|
10
|
+
// Field 2026-10-04: every piece was here, so the read waited for nothing and
|
|
11
|
+
// stated nothing, and the torrent was removed under it.
|
|
12
|
+
const torrent = {};
|
|
13
|
+
const used = filesInUse({ torrent, windows: [], openReads: [{ torrent, fileIndex: 0 }] });
|
|
14
|
+
assert.deepEqual([...used], [0]);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("a read of another torrent does not count", () => {
|
|
18
|
+
const used = filesInUse({ torrent: {}, windows: [], openReads: [{ torrent: {}, fileIndex: 0 }] });
|
|
19
|
+
assert.equal(used.size, 0);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test("stated needs count, and the fills that read nothing do not", () => {
|
|
23
|
+
const used = filesInUse({
|
|
24
|
+
torrent: {},
|
|
25
|
+
windows: [
|
|
26
|
+
{ claimant: "reader:7", fileIndex: 2 },
|
|
27
|
+
{ claimant: "file-edges:1", fileIndex: 1 },
|
|
28
|
+
{ claimant: "torrent-fill:x", fileIndex: 3 },
|
|
29
|
+
{ claimant: "background-fill:x", fileIndex: 4 }
|
|
30
|
+
],
|
|
31
|
+
openReads: []
|
|
32
|
+
});
|
|
33
|
+
assert.deepEqual([...used], [2]);
|
|
34
|
+
});
|