@torrent-tv/proxy 2.64.9 → 2.66.0
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 +20 -0
- package/docs/container-architecture.md +32 -0
- package/package.json +1 -1
- package/routes/api/sources/warm/post.js +57 -2
- package/routes/api/transcode-sessions/post.js +11 -1
- package/server.js +24 -1
- package/services/audio-inventory.js +411 -0
- package/services/hls-session-manager.js +396 -24
- package/services/playback-planner.js +133 -2
- package/services/sidecar-files.js +352 -0
- package/services/torrent-worker/client.js +14 -0
- package/services/torrent-worker/container-tracks.js +243 -0
- package/services/torrent-worker/pool-adapter.js +38 -0
- package/services/torrent-worker/protocol.js +8 -0
- package/services/torrent-worker/worker.js +19 -0
- package/services/tracks/index.js +7 -1
- package/test/audio-inventory.test.js +177 -0
- package/test/sidecar-files.test.js +178 -0
- package/services/tracks/ExternalSubtitleFile.js +0 -27
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Which files of a torrent are one video file's sound and subtitles.
|
|
3
|
+
*
|
|
4
|
+
* The case these were written against is a real torrent, and every name below
|
|
5
|
+
* is copied from it: `Drifters`, twelve episodes as `.mkv` in the root, twelve
|
|
6
|
+
* Russian soundtracks as `.mka` under `Rus Sound/`, twelve subtitle files under
|
|
7
|
+
* `Sub/[Stan WarHammer & Nesitach]/`, all sharing one base name per episode.
|
|
8
|
+
*
|
|
9
|
+
* The rule that matters most is the one about a torrent with SEVERAL videos: a
|
|
10
|
+
* sidecar is taken only when its name pairs with this episode's, because a
|
|
11
|
+
* wrong pairing would put the sound of episode 2 over the picture of episode 7
|
|
12
|
+
* and nothing downstream could notice.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import test from "node:test";
|
|
16
|
+
import assert from "node:assert/strict";
|
|
17
|
+
import {
|
|
18
|
+
baseNameOf,
|
|
19
|
+
bracketTokensOf,
|
|
20
|
+
countVideoFiles,
|
|
21
|
+
extensionOf,
|
|
22
|
+
matchSidecarFiles,
|
|
23
|
+
namesPair,
|
|
24
|
+
splitTorrentPath
|
|
25
|
+
} from "../services/sidecar-files.js";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* The Drifters torrent, as WebTorrent reports it: every path prefixed with the
|
|
29
|
+
* torrent's own name.
|
|
30
|
+
*
|
|
31
|
+
* @param {number} episodes
|
|
32
|
+
* @returns {Array<{ path: string, name: string, length: number }>}
|
|
33
|
+
*/
|
|
34
|
+
function driftersFiles(episodes = 3) {
|
|
35
|
+
const files = [];
|
|
36
|
+
const push = (relative, length) => {
|
|
37
|
+
const name = relative.slice(relative.lastIndexOf("/") + 1);
|
|
38
|
+
files.push({ path: `Drifters/${relative}`, name, length });
|
|
39
|
+
};
|
|
40
|
+
for (let episode = 1; episode <= episodes; episode += 1) {
|
|
41
|
+
const stem = `[HorribleSubs] Drifters - ${String(episode).padStart(2, "0")} [1080p]`;
|
|
42
|
+
push(`Sub/[Stan WarHammer & Nesitach]/${stem}.ass`, 29_000);
|
|
43
|
+
push(`Rus Sound/${stem}.mka`, 30_000_000);
|
|
44
|
+
push(`${stem}.mkv`, 566_000_000);
|
|
45
|
+
}
|
|
46
|
+
return files;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
test("a path is split into folders and a name, with the torrent's own name removed", () => {
|
|
50
|
+
const split = splitTorrentPath("Drifters/Rus Sound/[HorribleSubs] Drifters - 02 [1080p].mka", "Drifters");
|
|
51
|
+
assert.deepEqual(split.folders, ["Rus Sound"]);
|
|
52
|
+
assert.equal(split.name, "[HorribleSubs] Drifters - 02 [1080p].mka");
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("a path that does not begin with the torrent name is left alone", () => {
|
|
56
|
+
const split = splitTorrentPath("Sub/x.ass", "Drifters");
|
|
57
|
+
assert.deepEqual(split.folders, ["Sub"]);
|
|
58
|
+
assert.equal(split.name, "x.ass");
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("extension and base name", () => {
|
|
62
|
+
assert.equal(extensionOf("[HorribleSubs] Drifters - 02 [1080p].mka"), ".mka");
|
|
63
|
+
assert.equal(extensionOf("no-extension"), "");
|
|
64
|
+
assert.equal(extensionOf(".hidden"), "");
|
|
65
|
+
assert.equal(baseNameOf("a.b.mkv"), "a.b");
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
test("bracketed groups are read in order", () => {
|
|
69
|
+
assert.deepEqual(
|
|
70
|
+
bracketTokensOf("[HorribleSubs] Drifters - 02 [1080p]"),
|
|
71
|
+
["HorribleSubs", "1080p"]
|
|
72
|
+
);
|
|
73
|
+
assert.deepEqual(bracketTokensOf("nothing here"), []);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
test("equal base names pair, whatever folder each sits in", () => {
|
|
77
|
+
assert.equal(
|
|
78
|
+
namesPair("[HorribleSubs] Drifters - 02 [1080p].mka", "[HorribleSubs] Drifters - 02 [1080p].mkv"),
|
|
79
|
+
true
|
|
80
|
+
);
|
|
81
|
+
assert.equal(
|
|
82
|
+
namesPair("[HorribleSubs] Drifters - 03 [1080p].mka", "[HorribleSubs] Drifters - 02 [1080p].mkv"),
|
|
83
|
+
false
|
|
84
|
+
);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("a shared release hash pairs two differently named files", () => {
|
|
88
|
+
assert.equal(namesPair("Ep01_rus [A1B2C3D4].mka", "[Group] Ep 01 [A1B2C3D4].mkv"), true);
|
|
89
|
+
assert.equal(namesPair("Ep01_rus [11112222].mka", "[Group] Ep 01 [A1B2C3D4].mkv"), false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("the sound and the subtitles of THIS episode are found, and no other's", () => {
|
|
93
|
+
const files = driftersFiles(3);
|
|
94
|
+
// Episode 2's picture: index 5 in the list built above.
|
|
95
|
+
const videoIndex = files.findIndex((file) => file.name === "[HorribleSubs] Drifters - 02 [1080p].mkv");
|
|
96
|
+
const matched = matchSidecarFiles({
|
|
97
|
+
files,
|
|
98
|
+
videoIndex,
|
|
99
|
+
torrentName: "Drifters",
|
|
100
|
+
videoCount: countVideoFiles(files)
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
assert.equal(matched.audio.length, 1);
|
|
104
|
+
assert.equal(matched.audio[0].name, "[HorribleSubs] Drifters - 02 [1080p].mka");
|
|
105
|
+
assert.deepEqual(matched.audio[0].folders, ["Rus Sound"]);
|
|
106
|
+
assert.equal(matched.audio[0].extension, ".mka");
|
|
107
|
+
// A `.mka` is Matroska, so its own track table can be read — which is the
|
|
108
|
+
// whole reason no new container class was needed for a separate soundtrack.
|
|
109
|
+
assert.equal(matched.audio[0].declaresTracks, true);
|
|
110
|
+
|
|
111
|
+
assert.equal(matched.subtitles.length, 1);
|
|
112
|
+
assert.equal(matched.subtitles[0].name, "[HorribleSubs] Drifters - 02 [1080p].ass");
|
|
113
|
+
assert.deepEqual(matched.subtitles[0].folders, ["Sub", "[Stan WarHammer & Nesitach]"]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("the picture itself is never its own sidecar", () => {
|
|
117
|
+
const files = driftersFiles(1);
|
|
118
|
+
const videoIndex = files.findIndex((file) => file.name.endsWith(".mkv"));
|
|
119
|
+
const matched = matchSidecarFiles({ files, videoIndex, torrentName: "Drifters", videoCount: 1 });
|
|
120
|
+
assert.equal(matched.audio.some((entry) => entry.fileIndex === videoIndex), false);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test("a torrent with one picture takes every sidecar, however it is named", () => {
|
|
124
|
+
const files = [
|
|
125
|
+
{ path: "Film/Film.2019.1080p.mkv", name: "Film.2019.1080p.mkv", length: 5_000_000_000 },
|
|
126
|
+
{ path: "Film/Rus.mka", name: "Rus.mka", length: 300_000_000 },
|
|
127
|
+
{ path: "Film/subs/forced.srt", name: "forced.srt", length: 4_000 }
|
|
128
|
+
];
|
|
129
|
+
const matched = matchSidecarFiles({
|
|
130
|
+
files,
|
|
131
|
+
videoIndex: 0,
|
|
132
|
+
torrentName: "Film",
|
|
133
|
+
videoCount: countVideoFiles(files)
|
|
134
|
+
});
|
|
135
|
+
assert.deepEqual(matched.audio.map((entry) => entry.name), ["Rus.mka"]);
|
|
136
|
+
assert.deepEqual(matched.subtitles.map((entry) => entry.name), ["forced.srt"]);
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test("a torrent with several pictures does NOT take a sidecar that names none of them", () => {
|
|
140
|
+
const files = [
|
|
141
|
+
{ path: "Pack/Ep01.mkv", name: "Ep01.mkv", length: 100 },
|
|
142
|
+
{ path: "Pack/Ep02.mkv", name: "Ep02.mkv", length: 100 },
|
|
143
|
+
{ path: "Pack/Sound/Something Else.mka", name: "Something Else.mka", length: 100 }
|
|
144
|
+
];
|
|
145
|
+
const matched = matchSidecarFiles({
|
|
146
|
+
files,
|
|
147
|
+
videoIndex: 0,
|
|
148
|
+
torrentName: "Pack",
|
|
149
|
+
videoCount: countVideoFiles(files)
|
|
150
|
+
});
|
|
151
|
+
assert.deepEqual(matched.audio, []);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("files of no interest are ignored", () => {
|
|
155
|
+
const files = [
|
|
156
|
+
{ path: "X/film.mkv", name: "film.mkv", length: 100 },
|
|
157
|
+
{ path: "X/film.nfo", name: "film.nfo", length: 10 },
|
|
158
|
+
{ path: "X/cover.jpg", name: "cover.jpg", length: 10 }
|
|
159
|
+
];
|
|
160
|
+
const matched = matchSidecarFiles({ files, videoIndex: 0, torrentName: "X", videoCount: 1 });
|
|
161
|
+
assert.deepEqual(matched.audio, []);
|
|
162
|
+
assert.deepEqual(matched.subtitles, []);
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("a raw elementary stream is recognised but declares no tracks of its own", () => {
|
|
166
|
+
const files = [
|
|
167
|
+
{ path: "X/film.mkv", name: "film.mkv", length: 100 },
|
|
168
|
+
{ path: "X/film.ac3", name: "film.ac3", length: 100 }
|
|
169
|
+
];
|
|
170
|
+
const matched = matchSidecarFiles({ files, videoIndex: 0, torrentName: "X", videoCount: 1 });
|
|
171
|
+
assert.equal(matched.audio.length, 1);
|
|
172
|
+
assert.equal(matched.audio[0].declaresTracks, false);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
test("pictures are counted, and nothing else is", () => {
|
|
176
|
+
assert.equal(countVideoFiles(driftersFiles(12)), 12);
|
|
177
|
+
assert.equal(countVideoFiles([{ name: "a.mka" }, { name: "b.srt" }]), 0);
|
|
178
|
+
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file External subtitle file — not part of the media container, but shares TextSubtitleTrack API.
|
|
3
|
-
*
|
|
4
|
-
* A standalone .srt/.ass/.ssa/.vtt file beside the video in the same torrent.
|
|
5
|
-
* Has no container flags; language comes from filename suffix or franc detection.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export class ExternalSubtitleFile {
|
|
9
|
-
/**
|
|
10
|
-
* @param {object} params
|
|
11
|
-
* @param {string} params.fileName
|
|
12
|
-
* @param {number} params.fileIndex - Torrent file index.
|
|
13
|
-
* @param {string} params.extension - ".srt" etc. lowercased.
|
|
14
|
-
* @param {string} params.language - Hint from filename or "".
|
|
15
|
-
*/
|
|
16
|
-
constructor({ fileName, fileIndex, extension, language = "" }) {
|
|
17
|
-
this.fileName = fileName;
|
|
18
|
-
this.fileIndex = fileIndex;
|
|
19
|
-
this.extension = extension;
|
|
20
|
-
this.language = language;
|
|
21
|
-
this.type = "external-subtitle";
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
isTextBased() {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
}
|