@torrent-tv/proxy 2.89.6 → 2.89.8

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.
Files changed (92) hide show
  1. package/AGENTS.md +32 -12
  2. package/CHANGELOG.md +2340 -2330
  3. package/assets/calibration/audio/NOTICE.md +19 -19
  4. package/assets/calibration/audio/manifest.json +98 -98
  5. package/biome.json +412 -412
  6. package/docs/browser-proxy-contract.md +155 -155
  7. package/docs/container-architecture.md +273 -273
  8. package/docs/encode-run-state.md +1 -1
  9. package/docs/playback-start-forecast.md +113 -98
  10. package/package.json +51 -50
  11. package/routes/api/sources/files/get.js +106 -106
  12. package/scripts/build-audio-samples.mjs +31 -31
  13. package/services/encode/EncodeRun.js +67 -15
  14. package/services/encode/EncodeRuns.js +19 -7
  15. package/services/encode/OutputOpening.js +784 -784
  16. package/services/encode/audio-calibration.js +67 -67
  17. package/services/encode/audio-work-rate.js +15 -15
  18. package/services/encode/encode-exit.js +2 -0
  19. package/services/encode/hwaccel.js +1894 -1894
  20. package/services/encode/output/index.js +24 -24
  21. package/services/encode/piece-completeness.js +60 -2
  22. package/services/encode/run-command.js +757 -755
  23. package/services/encode/segment-formats/index.js +1 -0
  24. package/services/encode/segment-formats/mp4-boxes.js +8 -1
  25. package/services/media/ContainerOrchestrator.js +202 -202
  26. package/services/media/KeyframeTables.js +356 -356
  27. package/services/media/SubtitleCues.js +592 -592
  28. package/services/media/audio-inventory.js +299 -299
  29. package/services/media/container/AviContainer.js +268 -268
  30. package/services/media/container/Container.js +632 -632
  31. package/services/media/container/ContainerFactory.js +38 -38
  32. package/services/media/container/MatroskaContainer.js +1031 -1031
  33. package/services/media/container/Mp4Container.js +1354 -1354
  34. package/services/media/container/SubtitleFileContainer.js +339 -339
  35. package/services/media/container/ebml-stream.js +174 -174
  36. package/services/media/container/matroska-clusters.js +1165 -1165
  37. package/services/media/container/unavailable.js +77 -77
  38. package/services/media/tracks/ContainerTrack.js +84 -84
  39. package/services/media/tracks/TextSubtitleTrack.js +295 -295
  40. package/services/storage/piece-store/shared-piece-store.js +1937 -1937
  41. package/services/storage/segment-store/SegmentStore.js +1294 -1294
  42. package/services/torrent/Contents.js +382 -382
  43. package/services/torrent/episode-naming.js +182 -182
  44. package/services/torrent/torrent-pool.js +2688 -2688
  45. package/services/torrent/worker/client.js +836 -836
  46. package/services/torrent/worker/held-bytes.js +231 -231
  47. package/services/torrent/worker/worker.js +1137 -1137
  48. package/services/transport/data-channel-handler.js +1373 -1373
  49. package/services/viewer/PriorityMap.js +421 -421
  50. package/test/audio-calibration.test.js +23 -23
  51. package/test/auto-quality-step.test.js +762 -762
  52. package/test/coalesce.test.js +39 -39
  53. package/test/coverage-follows-the-disk.test.js +5 -0
  54. package/test/decode-cost.test.js +475 -475
  55. package/test/encode-run.test.js +58 -0
  56. package/test/episode-naming.test.js +131 -131
  57. package/test/failed-encoder-still-serves-what-exists.test.js +1 -1
  58. package/test/fixtures/audio-frame-boundary.json +1 -1
  59. package/test/fixtures/refused-audio-20261002.json +10 -0
  60. package/test/fmp4-presentation-clock.test.js +4 -2
  61. package/test/format-keyed-outputs.test.js +78 -78
  62. package/test/health-metrics.test.js +11 -13
  63. package/test/held-read-ownership.test.js +77 -77
  64. package/test/helpers/encode-run.js +14 -6
  65. package/test/helpers/held-file.js +77 -77
  66. package/test/helpers/manager.js +62 -62
  67. package/test/helpers/matroska-file.js +296 -296
  68. package/test/keyframe-table-budget.test.js +123 -123
  69. package/test/matroska-cluster-discovery.test.js +323 -323
  70. package/test/one-output-one-session.test.js +273 -273
  71. package/test/output-speed.test.js +6 -1
  72. package/test/piece-completeness.test.js +37 -1
  73. package/test/quality-variants.test.js +1274 -1270
  74. package/test/rate-control-in-the-output.test.js +4 -3
  75. package/test/reads-not-yet-arrived.test.js +218 -218
  76. package/test/seek-frees-the-viewers-requests.test.js +1 -1
  77. package/test/segment-closed-on-both-branches.test.js +114 -114
  78. package/test/segment-judged-by-its-own-header.test.js +222 -222
  79. package/test/segment-serve-wiring.test.js +481 -481
  80. package/test/serving-existing-output.test.js +111 -111
  81. package/test/source-files-route.test.js +113 -113
  82. package/test/subtitle-cue-walk.test.js +385 -385
  83. package/test/subtitle-metadata.test.js +15 -15
  84. package/test/subtitle-plan-waits.test.js +125 -125
  85. package/test/subtitle-track-numbering.test.js +391 -391
  86. package/test/two-viewers-one-picture.test.js +508 -508
  87. package/test/viewer-requests-admission.test.js +92 -92
  88. package/test/viewer-starts-on-map.test.js +27 -27
  89. package/test/viewer-states.test.js +5 -2
  90. package/test/viewing-generations.test.js +390 -390
  91. package/test/whole-file-held-read.test.js +30 -30
  92. package/utils/coalesce.js +41 -41
@@ -1,182 +1,182 @@
1
- /**
2
- * @file What a picture's NAME says about which episode it is.
3
- *
4
- * A season pack states its structure in names — `s01e02_Murder.in.the.Mews.avi`,
5
- * `Reacher.S04E01.1080p.rus.LostFilm.TV.mkv`, `[HorribleSubs] Drifters - 01
6
- * [1080p].mkv` under a folder, or an episode number under `Season_01/`. This
7
- * reads that statement and nothing else: which season, which episode numbers,
8
- * which part of an episode, whether it is a special, and the text on either
9
- * side of the marker.
10
- *
11
- * The numbers are the RELEASE's numbering. They are often the same as a
12
- * metadata provider's and sometimes not — one release numbers the two parts of
13
- * a feature-length episode as two episodes, which shifts every number after
14
- * them — so nothing here claims to know which episode of the show a file is.
15
- * That is decided later, against the provider's own list, by whoever holds it.
16
- *
17
- * A name without a marker returns `null`: absence of a number is absence of
18
- * information, not evidence that the file is not an episode.
19
- */
20
-
21
- import { stripExtension } from "./naming.js";
22
-
23
- /** Most episodes one file may claim before the claim is read as noise. */
24
- const MAX_EPISODES_PER_FILE = 8;
25
-
26
- /** Longest hint returned, in characters; a name longer than this is not a title. */
27
- const MAX_HINT_LENGTH = 160;
28
-
29
- /** `S01E02`, `s03.e01`, `S01E01E02`, `S01E01-E02`, `S01E01-02`. */
30
- const SEASON_EPISODE = /(?<![a-z0-9])s(\d{1,3})[ ._]?e(\d{1,4})((?:-?e\d{1,4}|-\d{1,4})*)(?![0-9])/i;
31
-
32
- /** `1x02`; four-digit sides are picture sizes (`1920x1080`) and never match. */
33
- const CROSS = /(?<![a-z0-9])(\d{1,2})x(\d{2,3})(?![0-9a-z])/i;
34
-
35
- /** `E05`, `Ep 5`, `Episode.05`, `Серия 5` — an episode number with no season in it. */
36
- const EPISODE_ONLY = /(?<![\p{L}0-9])(?:e|ep|episode|серия)[ ._-]?(\d{1,4})(?![0-9])/iu;
37
-
38
- /** `Show - 01`, `Show - 01v2` — the numbering most anime releases use. */
39
- const DASH_NUMBER = / - (\d{1,4})(?:v\d)?(?=[ .[(]|$)/;
40
-
41
- /** A folder that names a season: `Season_01`, `Season 1`, `S01`, `Сезон 1`, `1 сезон`. */
42
- const SEASON_FOLDER = /^(?:season|сезон|s)[ ._-]*(\d{1,3})$|^(\d{1,3})[ ._-]*(?:season|сезон)$/iu;
43
-
44
- /** `Part.1`, `Pt 2`, `Часть 1`. */
45
- const PART = /(?<![\p{L}0-9])(?:part|pt|часть)[ ._-]?(\d{1,2})(?![0-9])/iu;
46
-
47
- /** `Special`, `Specials`, `SP01`. */
48
- const SPECIAL = /(?<![\p{L}])(?:specials?|sp\d{1,3})(?![\p{L}])/iu;
49
-
50
- /** Separators a release puts between words, trimmed off the ends of a hint. */
51
- const EDGE_SEPARATORS = /^[\s._\-–—]+|[\s._\-–—]+$/g;
52
-
53
- /**
54
- * An episode marker read from a name.
55
- *
56
- * @typedef {object} EpisodeMarker
57
- * @property {number | null} season - `null` when neither the name nor a folder states one.
58
- * @property {number[]} episodes - The release's numbers, one or more.
59
- * @property {number | null} part - Which part of one episode, when the name says so.
60
- * @property {boolean} special - Season 0, or a name calling itself a special.
61
- * @property {string} showHint - The name's text before the marker, as written.
62
- * @property {string} titleHint - The name's text after the marker, part removed, as written.
63
- */
64
-
65
- /**
66
- * Trim a piece of a name to a hint.
67
- *
68
- * @param {string} text
69
- * @returns {string}
70
- */
71
- function hint(text) {
72
- return String(text ?? "").replace(EDGE_SEPARATORS, "").slice(0, MAX_HINT_LENGTH);
73
- }
74
-
75
- /**
76
- * The numbers after the first episode in `S01E01E02` or `S01E01-03`.
77
- *
78
- * A `-` joins a range, a repeated `E` lists another episode.
79
- *
80
- * @param {number} first
81
- * @param {string} rest
82
- * @returns {number[] | null} `null` when the claim is longer than any file carries.
83
- */
84
- function episodesFrom(first, rest) {
85
- const episodes = [first];
86
- for (const token of rest.match(/-?e?\d+/gi) ?? []) {
87
- const value = Number(token.replace(/[^0-9]/g, ""));
88
- const last = episodes[episodes.length - 1];
89
- if (token.startsWith("-") && value > last) {
90
- for (let next = last + 1; next <= value; next += 1) {
91
- episodes.push(next);
92
- if (episodes.length > MAX_EPISODES_PER_FILE) {
93
- return null;
94
- }
95
- }
96
- } else if (value !== last) {
97
- episodes.push(value);
98
- }
99
- }
100
- return episodes.length > MAX_EPISODES_PER_FILE ? null : episodes;
101
- }
102
-
103
- /**
104
- * The season the deepest season-naming folder states, if any does.
105
- *
106
- * @param {string[]} folders
107
- * @returns {number | null}
108
- */
109
- function seasonFromFolders(folders) {
110
- for (let index = folders.length - 1; index >= 0; index -= 1) {
111
- const match = SEASON_FOLDER.exec(String(folders[index] ?? "").trim());
112
- if (match) {
113
- return Number(match[1] ?? match[2]);
114
- }
115
- }
116
- return null;
117
- }
118
-
119
- /**
120
- * Which episode a picture's name says it is.
121
- *
122
- * @param {{ name: string, folders?: string[] }} file - File name and the folders above it.
123
- * @returns {EpisodeMarker | null}
124
- */
125
- export function readEpisodeMarker({ name, folders = [] }) {
126
- const stem = stripExtension(String(name ?? ""));
127
- const folderSeason = seasonFromFolders(Array.isArray(folders) ? folders : []);
128
-
129
- let season = null;
130
- let episodes = null;
131
- let before = "";
132
- let after = "";
133
-
134
- const seasonEpisode = SEASON_EPISODE.exec(stem);
135
- const cross = seasonEpisode ? null : CROSS.exec(stem);
136
- const episodeOnly = seasonEpisode || cross ? null : EPISODE_ONLY.exec(stem);
137
- const dash = seasonEpisode || cross || episodeOnly ? null : DASH_NUMBER.exec(stem);
138
- const match = seasonEpisode ?? cross ?? episodeOnly ?? dash;
139
- if (!match) {
140
- return null;
141
- }
142
- before = stem.slice(0, match.index);
143
- after = stem.slice(match.index + match[0].length);
144
-
145
- if (seasonEpisode) {
146
- season = Number(seasonEpisode[1]);
147
- episodes = episodesFrom(Number(seasonEpisode[2]), seasonEpisode[3] ?? "");
148
- } else if (cross) {
149
- season = Number(cross[1]);
150
- episodes = [Number(cross[2])];
151
- } else {
152
- // `Show S1 - 01` states its season just before the number; a folder is the
153
- // other place a release puts it.
154
- const trailingSeason = /(?<![a-z0-9])s(\d{1,3})[\s._-]*$/i.exec(before);
155
- season = trailingSeason ? Number(trailingSeason[1]) : folderSeason;
156
- if (trailingSeason) {
157
- before = before.slice(0, trailingSeason.index);
158
- }
159
- episodes = [Number(match[1])];
160
- }
161
- // A year where an episode number would be is a year far more often than an
162
- // episode: `Film - 2024 [1080p]`, and `WALL-E.2008.mkv` reads as `E` then
163
- // `2008`. Only the markers without a season can be fooled this way.
164
- if ((dash || episodeOnly) && /^(?:19|20)\d\d$/.test(match[1])) {
165
- return null;
166
- }
167
- if (!episodes) {
168
- return null;
169
- }
170
-
171
- const part = PART.exec(after);
172
- const titleText = part ? `${after.slice(0, part.index)} ${after.slice(part.index + part[0].length)}` : after;
173
-
174
- return {
175
- season,
176
- episodes,
177
- part: part ? Number(part[1]) : null,
178
- special: season === 0 || SPECIAL.test(stem),
179
- showHint: hint(before),
180
- titleHint: hint(titleText)
181
- };
182
- }
1
+ /**
2
+ * @file What a picture's NAME says about which episode it is.
3
+ *
4
+ * A season pack states its structure in names — `s01e02_Murder.in.the.Mews.avi`,
5
+ * `Reacher.S04E01.1080p.rus.LostFilm.TV.mkv`, `[HorribleSubs] Drifters - 01
6
+ * [1080p].mkv` under a folder, or an episode number under `Season_01/`. This
7
+ * reads that statement and nothing else: which season, which episode numbers,
8
+ * which part of an episode, whether it is a special, and the text on either
9
+ * side of the marker.
10
+ *
11
+ * The numbers are the RELEASE's numbering. They are often the same as a
12
+ * metadata provider's and sometimes not — one release numbers the two parts of
13
+ * a feature-length episode as two episodes, which shifts every number after
14
+ * them — so nothing here claims to know which episode of the show a file is.
15
+ * That is decided later, against the provider's own list, by whoever holds it.
16
+ *
17
+ * A name without a marker returns `null`: absence of a number is absence of
18
+ * information, not evidence that the file is not an episode.
19
+ */
20
+
21
+ import { stripExtension } from "./naming.js";
22
+
23
+ /** Most episodes one file may claim before the claim is read as noise. */
24
+ const MAX_EPISODES_PER_FILE = 8;
25
+
26
+ /** Longest hint returned, in characters; a name longer than this is not a title. */
27
+ const MAX_HINT_LENGTH = 160;
28
+
29
+ /** `S01E02`, `s03.e01`, `S01E01E02`, `S01E01-E02`, `S01E01-02`. */
30
+ const SEASON_EPISODE = /(?<![a-z0-9])s(\d{1,3})[ ._]?e(\d{1,4})((?:-?e\d{1,4}|-\d{1,4})*)(?![0-9])/i;
31
+
32
+ /** `1x02`; four-digit sides are picture sizes (`1920x1080`) and never match. */
33
+ const CROSS = /(?<![a-z0-9])(\d{1,2})x(\d{2,3})(?![0-9a-z])/i;
34
+
35
+ /** `E05`, `Ep 5`, `Episode.05`, `Серия 5` — an episode number with no season in it. */
36
+ const EPISODE_ONLY = /(?<![\p{L}0-9])(?:e|ep|episode|серия)[ ._-]?(\d{1,4})(?![0-9])/iu;
37
+
38
+ /** `Show - 01`, `Show - 01v2` — the numbering most anime releases use. */
39
+ const DASH_NUMBER = / - (\d{1,4})(?:v\d)?(?=[ .[(]|$)/;
40
+
41
+ /** A folder that names a season: `Season_01`, `Season 1`, `S01`, `Сезон 1`, `1 сезон`. */
42
+ const SEASON_FOLDER = /^(?:season|сезон|s)[ ._-]*(\d{1,3})$|^(\d{1,3})[ ._-]*(?:season|сезон)$/iu;
43
+
44
+ /** `Part.1`, `Pt 2`, `Часть 1`. */
45
+ const PART = /(?<![\p{L}0-9])(?:part|pt|часть)[ ._-]?(\d{1,2})(?![0-9])/iu;
46
+
47
+ /** `Special`, `Specials`, `SP01`. */
48
+ const SPECIAL = /(?<![\p{L}])(?:specials?|sp\d{1,3})(?![\p{L}])/iu;
49
+
50
+ /** Separators a release puts between words, trimmed off the ends of a hint. */
51
+ const EDGE_SEPARATORS = /^[\s._\-–—]+|[\s._\-–—]+$/g;
52
+
53
+ /**
54
+ * An episode marker read from a name.
55
+ *
56
+ * @typedef {object} EpisodeMarker
57
+ * @property {number | null} season - `null` when neither the name nor a folder states one.
58
+ * @property {number[]} episodes - The release's numbers, one or more.
59
+ * @property {number | null} part - Which part of one episode, when the name says so.
60
+ * @property {boolean} special - Season 0, or a name calling itself a special.
61
+ * @property {string} showHint - The name's text before the marker, as written.
62
+ * @property {string} titleHint - The name's text after the marker, part removed, as written.
63
+ */
64
+
65
+ /**
66
+ * Trim a piece of a name to a hint.
67
+ *
68
+ * @param {string} text
69
+ * @returns {string}
70
+ */
71
+ function hint(text) {
72
+ return String(text ?? "").replace(EDGE_SEPARATORS, "").slice(0, MAX_HINT_LENGTH);
73
+ }
74
+
75
+ /**
76
+ * The numbers after the first episode in `S01E01E02` or `S01E01-03`.
77
+ *
78
+ * A `-` joins a range, a repeated `E` lists another episode.
79
+ *
80
+ * @param {number} first
81
+ * @param {string} rest
82
+ * @returns {number[] | null} `null` when the claim is longer than any file carries.
83
+ */
84
+ function episodesFrom(first, rest) {
85
+ const episodes = [first];
86
+ for (const token of rest.match(/-?e?\d+/gi) ?? []) {
87
+ const value = Number(token.replace(/[^0-9]/g, ""));
88
+ const last = episodes[episodes.length - 1];
89
+ if (token.startsWith("-") && value > last) {
90
+ for (let next = last + 1; next <= value; next += 1) {
91
+ episodes.push(next);
92
+ if (episodes.length > MAX_EPISODES_PER_FILE) {
93
+ return null;
94
+ }
95
+ }
96
+ } else if (value !== last) {
97
+ episodes.push(value);
98
+ }
99
+ }
100
+ return episodes.length > MAX_EPISODES_PER_FILE ? null : episodes;
101
+ }
102
+
103
+ /**
104
+ * The season the deepest season-naming folder states, if any does.
105
+ *
106
+ * @param {string[]} folders
107
+ * @returns {number | null}
108
+ */
109
+ function seasonFromFolders(folders) {
110
+ for (let index = folders.length - 1; index >= 0; index -= 1) {
111
+ const match = SEASON_FOLDER.exec(String(folders[index] ?? "").trim());
112
+ if (match) {
113
+ return Number(match[1] ?? match[2]);
114
+ }
115
+ }
116
+ return null;
117
+ }
118
+
119
+ /**
120
+ * Which episode a picture's name says it is.
121
+ *
122
+ * @param {{ name: string, folders?: string[] }} file - File name and the folders above it.
123
+ * @returns {EpisodeMarker | null}
124
+ */
125
+ export function readEpisodeMarker({ name, folders = [] }) {
126
+ const stem = stripExtension(String(name ?? ""));
127
+ const folderSeason = seasonFromFolders(Array.isArray(folders) ? folders : []);
128
+
129
+ let season = null;
130
+ let episodes = null;
131
+ let before = "";
132
+ let after = "";
133
+
134
+ const seasonEpisode = SEASON_EPISODE.exec(stem);
135
+ const cross = seasonEpisode ? null : CROSS.exec(stem);
136
+ const episodeOnly = seasonEpisode || cross ? null : EPISODE_ONLY.exec(stem);
137
+ const dash = seasonEpisode || cross || episodeOnly ? null : DASH_NUMBER.exec(stem);
138
+ const match = seasonEpisode ?? cross ?? episodeOnly ?? dash;
139
+ if (!match) {
140
+ return null;
141
+ }
142
+ before = stem.slice(0, match.index);
143
+ after = stem.slice(match.index + match[0].length);
144
+
145
+ if (seasonEpisode) {
146
+ season = Number(seasonEpisode[1]);
147
+ episodes = episodesFrom(Number(seasonEpisode[2]), seasonEpisode[3] ?? "");
148
+ } else if (cross) {
149
+ season = Number(cross[1]);
150
+ episodes = [Number(cross[2])];
151
+ } else {
152
+ // `Show S1 - 01` states its season just before the number; a folder is the
153
+ // other place a release puts it.
154
+ const trailingSeason = /(?<![a-z0-9])s(\d{1,3})[\s._-]*$/i.exec(before);
155
+ season = trailingSeason ? Number(trailingSeason[1]) : folderSeason;
156
+ if (trailingSeason) {
157
+ before = before.slice(0, trailingSeason.index);
158
+ }
159
+ episodes = [Number(match[1])];
160
+ }
161
+ // A year where an episode number would be is a year far more often than an
162
+ // episode: `Film - 2024 [1080p]`, and `WALL-E.2008.mkv` reads as `E` then
163
+ // `2008`. Only the markers without a season can be fooled this way.
164
+ if ((dash || episodeOnly) && /^(?:19|20)\d\d$/.test(match[1])) {
165
+ return null;
166
+ }
167
+ if (!episodes) {
168
+ return null;
169
+ }
170
+
171
+ const part = PART.exec(after);
172
+ const titleText = part ? `${after.slice(0, part.index)} ${after.slice(part.index + part[0].length)}` : after;
173
+
174
+ return {
175
+ season,
176
+ episodes,
177
+ part: part ? Number(part[1]) : null,
178
+ special: season === 0 || SPECIAL.test(stem),
179
+ showHint: hint(before),
180
+ titleHint: hint(titleText)
181
+ };
182
+ }