@torrent-tv/proxy 2.89.17 → 2.89.19

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 (300) hide show
  1. package/README.md +14 -3
  2. package/package.json +10 -2
  3. package/server.js +0 -11
  4. package/utils/client-log-file.js +147 -42
  5. package/.claude/commands/opsx/apply.md +0 -155
  6. package/.claude/commands/opsx/archive.md +0 -160
  7. package/.claude/commands/opsx/explore.md +0 -174
  8. package/.claude/commands/opsx/propose.md +0 -109
  9. package/.claude/commands/opsx/sync.md +0 -143
  10. package/.claude/skills/openspec-apply-change/SKILL.md +0 -159
  11. package/.claude/skills/openspec-archive-change/SKILL.md +0 -117
  12. package/.claude/skills/openspec-explore/SKILL.md +0 -289
  13. package/.claude/skills/openspec-propose/SKILL.md +0 -113
  14. package/.claude/skills/openspec-sync-specs/SKILL.md +0 -147
  15. package/.dockerignore +0 -5
  16. package/AGENTS.md +0 -363
  17. package/CHANGELOG.md +0 -2377
  18. package/Dockerfile +0 -30
  19. package/biome.json +0 -412
  20. package/corpus.tmp.json +0 -1
  21. package/docs/browser-proxy-contract.md +0 -155
  22. package/docs/container-architecture.md +0 -273
  23. package/docs/download-architecture.md +0 -261
  24. package/docs/encode-architecture.md +0 -765
  25. package/docs/encode-run-state.md +0 -96
  26. package/docs/logs.md +0 -151
  27. package/docs/playback-start-forecast.md +0 -113
  28. package/docs/socks5-egress-research.md +0 -114
  29. package/docs/storage-architecture.md +0 -273
  30. package/knip.json +0 -14
  31. package/main2.cpuprofile +0 -1
  32. package/openspec/changes/adaptive-bitrate/.openspec.yaml +0 -0
  33. package/openspec/changes/adaptive-bitrate/design.md +0 -92
  34. package/openspec/changes/adaptive-bitrate/proposal.md +0 -56
  35. package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +0 -51
  36. package/openspec/changes/adaptive-bitrate/tasks.md +0 -40
  37. package/openspec/changes/binary-distribution/.openspec.yaml +0 -2
  38. package/openspec/changes/binary-distribution/proposal.md +0 -53
  39. package/openspec/changes/chunked-request-bodies/design.md +0 -81
  40. package/openspec/changes/chunked-request-bodies/proposal.md +0 -53
  41. package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +0 -30
  42. package/openspec/changes/chunked-request-bodies/tasks.md +0 -40
  43. package/openspec/changes/cold-start/design.md +0 -98
  44. package/openspec/changes/cold-start/proposal.md +0 -62
  45. package/openspec/changes/cold-start/specs/cold-start/spec.md +0 -47
  46. package/openspec/changes/cold-start/tasks.md +0 -48
  47. package/openspec/changes/disk-cap/.openspec.yaml +0 -2
  48. package/openspec/changes/disk-cap/proposal.md +0 -37
  49. package/openspec/changes/disk-cap/specs/disk-cap/spec.md +0 -25
  50. package/openspec/changes/disk-cap/tasks.md +0 -19
  51. package/openspec/changes/proxy-observability/.openspec.yaml +0 -2
  52. package/openspec/changes/proxy-observability/design.md +0 -38
  53. package/openspec/changes/proxy-observability/proposal.md +0 -49
  54. package/openspec/changes/proxy-observability/specs/observability/spec.md +0 -33
  55. package/openspec/changes/proxy-observability/tasks.md +0 -21
  56. package/openspec/changes/seek-debounce/.openspec.yaml +0 -0
  57. package/openspec/changes/seek-debounce/design.md +0 -118
  58. package/openspec/changes/seek-debounce/proposal.md +0 -59
  59. package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +0 -41
  60. package/openspec/changes/seek-debounce/tasks.md +0 -33
  61. package/openspec/changes/subtitle-language/.openspec.yaml +0 -2
  62. package/openspec/changes/subtitle-language/proposal.md +0 -50
  63. package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +0 -34
  64. package/openspec/changes/subtitle-language/tasks.md +0 -26
  65. package/openspec/changes/track-selection/.openspec.yaml +0 -2
  66. package/openspec/changes/track-selection/design.md +0 -47
  67. package/openspec/changes/track-selection/proposal.md +0 -45
  68. package/openspec/changes/track-selection/specs/track-selection/spec.md +0 -41
  69. package/openspec/changes/track-selection/tasks.md +0 -20
  70. package/openspec/changes/transcode-quality/.openspec.yaml +0 -2
  71. package/openspec/changes/transcode-quality/proposal.md +0 -51
  72. package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +0 -139
  73. package/openspec/changes/transcode-quality/tasks.md +0 -63
  74. package/openspec/config.yaml +0 -38
  75. package/public/.well-known/appspecific/com.chrome.devtools.json +0 -1
  76. package/research/delivery-wedge-stand-ladder-2026-09-13.md +0 -329
  77. package/research/double-spawn-2026-09-10.md +0 -171
  78. package/research/handover-2026-09-13.md +0 -209
  79. package/research/handover-reader-claims-removal-2026-09-12.md +0 -166
  80. package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +0 -175
  81. package/research/preset-benchmark-size-scaling-2026-08-22.md +0 -55
  82. package/research/priority-map-is-the-truth-2026-09-12.md +0 -225
  83. package/research/valgrind-replica-2026-08-27/logs.tar.gz +0 -0
  84. package/scripts/build-audio-samples.mjs +0 -31
  85. package/scripts/render-run-graph.js +0 -173
  86. package/test/asset-paths.test.js +0 -28
  87. package/test/audio-buffer-continuity.test.js +0 -77
  88. package/test/audio-calibration.test.js +0 -23
  89. package/test/audio-inventory.test.js +0 -212
  90. package/test/audio-track-bitrate.test.js +0 -40
  91. package/test/audio-warm-recorded.test.js +0 -63
  92. package/test/auto-quality-step.test.js +0 -762
  93. package/test/available-share.test.js +0 -76
  94. package/test/background-fill.test.js +0 -190
  95. package/test/client-log-file.test.js +0 -126
  96. package/test/client-logs-route.test.js +0 -34
  97. package/test/coalesce.test.js +0 -39
  98. package/test/completed-files.test.js +0 -115
  99. package/test/concurrent-cost.test.js +0 -148
  100. package/test/container-media-info.test.js +0 -228
  101. package/test/contention.test.js +0 -76
  102. package/test/core-dumps.test.js +0 -57
  103. package/test/coverage-follows-the-disk.test.js +0 -233
  104. package/test/coverage-map.test.js +0 -195
  105. package/test/cue-push-carries-what-the-walk-found.test.js +0 -58
  106. package/test/cut-times-timeline.test.js +0 -65
  107. package/test/cuts-follow-published-grid.test.js +0 -109
  108. package/test/data-channel-frame.test.js +0 -63
  109. package/test/data-channel-viewer-lifecycle.test.js +0 -193
  110. package/test/dead-torrent-handle.test.js +0 -52
  111. package/test/declared-rates.test.js +0 -116
  112. package/test/declared-tracks.test.js +0 -43
  113. package/test/decode-cost-fit.test.js +0 -202
  114. package/test/decode-cost.test.js +0 -475
  115. package/test/delivery-probe-field.test.js +0 -173
  116. package/test/delivery-probe.test.js +0 -382
  117. package/test/demand-register.test.js +0 -206
  118. package/test/dht-bootstrap.test.js +0 -72
  119. package/test/drain-threat.test.js +0 -65
  120. package/test/duplicate-add.test.js +0 -48
  121. package/test/encode-admission.test.js +0 -170
  122. package/test/encode-cost.test.js +0 -275
  123. package/test/encode-exit.test.js +0 -111
  124. package/test/encode-orchestrator.test.js +0 -426
  125. package/test/encode-plan-viewers.test.js +0 -725
  126. package/test/encode-plan.test.js +0 -545
  127. package/test/encode-run-state.test.js +0 -314
  128. package/test/encode-run.test.js +0 -248
  129. package/test/encode-slope.test.js +0 -46
  130. package/test/encoded-output.test.js +0 -39
  131. package/test/encoder-kinds.test.js +0 -122
  132. package/test/encoder-readings.test.js +0 -68
  133. package/test/episode-naming.test.js +0 -131
  134. package/test/every-claimant-is-told-its-share.test.js +0 -152
  135. package/test/failed-encoder-still-serves-what-exists.test.js +0 -354
  136. package/test/failed-start-breaker.test.js +0 -202
  137. package/test/fastest-wires.test.js +0 -143
  138. package/test/ffmpeg-stream-counts.test.js +0 -101
  139. package/test/file-edges.test.js +0 -191
  140. package/test/files-in-use.test.js +0 -34
  141. package/test/fixtures/audio-frame-boundary.json +0 -1
  142. package/test/fixtures/refused-audio-20261002.json +0 -10
  143. package/test/fmp4-presentation-clock.test.js +0 -148
  144. package/test/fmp4-self-contained.test.js +0 -80
  145. package/test/fmp4-timestamps.test.js +0 -140
  146. package/test/fmp4-track-completeness.test.js +0 -41
  147. package/test/format-keyed-outputs.test.js +0 -78
  148. package/test/health-metrics.test.js +0 -36
  149. package/test/held-read-ownership.test.js +0 -77
  150. package/test/held-request-width.test.js +0 -157
  151. package/test/helpers/encode-run.js +0 -248
  152. package/test/helpers/held-file.js +0 -77
  153. package/test/helpers/manager.js +0 -62
  154. package/test/helpers/matroska-file.js +0 -296
  155. package/test/helpers/output-spec.js +0 -63
  156. package/test/host-load.test.js +0 -68
  157. package/test/index-accuracy-is-about-the-file.test.js +0 -128
  158. package/test/init-compat.test.js +0 -219
  159. package/test/input-lost-quiets-the-plan.test.js +0 -261
  160. package/test/input-unavailable.test.js +0 -48
  161. package/test/installation-id.test.js +0 -39
  162. package/test/keeping-period.test.js +0 -83
  163. package/test/keyframe-index-accuracy.test.js +0 -188
  164. package/test/keyframe-table-budget.test.js +0 -123
  165. package/test/keyframes-belong-to-the-file.test.js +0 -249
  166. package/test/learned-median.test.js +0 -37
  167. package/test/letting-go-asks-who-is-watching.test.js +0 -191
  168. package/test/limit-row-by-area.test.js +0 -60
  169. package/test/link-budget.test.js +0 -111
  170. package/test/link-probe.test.js +0 -56
  171. package/test/local-encoding-data.test.js +0 -273
  172. package/test/logger-repeats.test.js +0 -134
  173. package/test/look-ahead-coverage.test.js +0 -56
  174. package/test/manager-surface.test.js +0 -291
  175. package/test/matroska-blocks.test.js +0 -0
  176. package/test/matroska-cluster-discovery.test.js +0 -323
  177. package/test/matroska-cues-track.test.js +0 -192
  178. package/test/media-time.test.js +0 -62
  179. package/test/memory-budget.test.js +0 -390
  180. package/test/move-cost.test.js +0 -216
  181. package/test/mp4-composition-times.test.js +0 -0
  182. package/test/mp4-subtitles.test.js +0 -173
  183. package/test/one-authority.test.js +0 -285
  184. package/test/one-budget.test.js +0 -149
  185. package/test/one-output-one-session.test.js +0 -273
  186. package/test/one-resource-divided.test.js +0 -185
  187. package/test/one-serving-output-per-height.test.js +0 -458
  188. package/test/orchestrator-wired.test.js +0 -207
  189. package/test/output-catalog.test.js +0 -39
  190. package/test/output-shape.test.js +0 -21
  191. package/test/output-spec.test.js +0 -251
  192. package/test/output-speed.test.js +0 -92
  193. package/test/outputs-being-prepared.test.js +0 -40
  194. package/test/packet-witness-ring.test.js +0 -236
  195. package/test/packet-witness.test.js +0 -148
  196. package/test/piece-completeness.test.js +0 -78
  197. package/test/piece-disk-store.test.js +0 -401
  198. package/test/piece-from-whole-file.test.js +0 -129
  199. package/test/piece-lru.test.js +0 -319
  200. package/test/piece-reader.test.js +0 -162
  201. package/test/piece-selection-offset.test.js +0 -85
  202. package/test/piece-store-eviction.test.js +0 -595
  203. package/test/piece-store-never-refuses.test.js +0 -153
  204. package/test/piece-store-reservations.test.js +0 -403
  205. package/test/piece-store-slow-disk.test.js +0 -159
  206. package/test/piece-tail.test.js +0 -97
  207. package/test/plan-host-timings.test.js +0 -68
  208. package/test/plan-is-steady.test.js +0 -225
  209. package/test/playback-readiness.test.js +0 -518
  210. package/test/playback-start-regressions.test.js +0 -101
  211. package/test/priority-map-download.test.js +0 -174
  212. package/test/priority-map-emptied.test.js +0 -204
  213. package/test/priority-map-per-output.test.js +0 -215
  214. package/test/priority-map.test.js +0 -163
  215. package/test/probe-wedge-certainty.test.js +0 -60
  216. package/test/produced-copy-choice.test.js +0 -213
  217. package/test/published-timeline.test.js +0 -83
  218. package/test/quality-offer.test.js +0 -160
  219. package/test/quality-variants.test.js +0 -1274
  220. package/test/rate-control-in-the-output.test.js +0 -181
  221. package/test/read-survives-withdrawal.test.js +0 -164
  222. package/test/read-window.test.js +0 -365
  223. package/test/reads-not-yet-arrived.test.js +0 -218
  224. package/test/resume-warm.test.js +0 -39
  225. package/test/run-graph-drift.test.js +0 -37
  226. package/test/run-inputs.test.js +0 -80
  227. package/test/run-intervals.test.js +0 -100
  228. package/test/run-position-follows-published-grid.test.js +0 -89
  229. package/test/run-progress.test.js +0 -64
  230. package/test/same-height-switch.test.js +0 -663
  231. package/test/seek-frees-the-viewers-requests.test.js +0 -217
  232. package/test/seek-landing.test.js +0 -109
  233. package/test/seek-target-not-superseded.test.js +0 -195
  234. package/test/segment-closed-on-both-branches.test.js +0 -114
  235. package/test/segment-cut-times.test.js +0 -63
  236. package/test/segment-demand.test.js +0 -80
  237. package/test/segment-hold-supersede.test.js +0 -91
  238. package/test/segment-judged-by-its-own-header.test.js +0 -222
  239. package/test/segment-serve-wiring.test.js +0 -481
  240. package/test/segment-store-eviction.test.js +0 -247
  241. package/test/segment-store.test.js +0 -352
  242. package/test/segment-true-start.test.js +0 -86
  243. package/test/segments-are-shared.test.js +0 -176
  244. package/test/send-chunk-bytes.test.js +0 -86
  245. package/test/serving-existing-output.test.js +0 -111
  246. package/test/serving-output.test.js +0 -102
  247. package/test/shared-piece-store.test.js +0 -310
  248. package/test/sidecar-naming.test.js +0 -142
  249. package/test/soundtrack-rate-bound.test.js +0 -289
  250. package/test/source-file.test.js +0 -133
  251. package/test/source-files-route.test.js +0 -113
  252. package/test/source-registry.test.js +0 -18
  253. package/test/split-a-stretch.test.js +0 -139
  254. package/test/stale-request-after-seek.test.js +0 -158
  255. package/test/startup-readings.test.js +0 -124
  256. package/test/stream-route.test.js +0 -237
  257. package/test/subtitle-cue-framing.test.js +0 -200
  258. package/test/subtitle-cue-source.test.js +0 -102
  259. package/test/subtitle-cue-walk.test.js +0 -385
  260. package/test/subtitle-cursor.test.js +0 -63
  261. package/test/subtitle-defaults.test.js +0 -118
  262. package/test/subtitle-language.test.js +0 -252
  263. package/test/subtitle-metadata.test.js +0 -15
  264. package/test/subtitle-plan-waits.test.js +0 -125
  265. package/test/subtitle-track-numbering.test.js +0 -391
  266. package/test/supply-margin.test.js +0 -190
  267. package/test/swarm-follows-readers.test.js +0 -174
  268. package/test/swarm-reach.test.js +0 -130
  269. package/test/swarm-selection.test.js +0 -220
  270. package/test/tail-duplication.test.js +0 -167
  271. package/test/the-channel-has-both-ends.test.js +0 -91
  272. package/test/the-torrent-thread-serves-bytes.test.js +0 -107
  273. package/test/thread-pool.test.js +0 -53
  274. package/test/timeline.test.js +0 -84
  275. package/test/torrent-contents.test.js +0 -235
  276. package/test/torrent-cost.test.js +0 -71
  277. package/test/torrent-files.test.js +0 -221
  278. package/test/torrent-naming.test.js +0 -255
  279. package/test/tracks-begin-together.test.js +0 -209
  280. package/test/tunnel-renewal.test.js +0 -167
  281. package/test/two-viewers-one-picture.test.js +0 -508
  282. package/test/upload-hurry.test.js +0 -186
  283. package/test/usrsctp-state.test.js +0 -131
  284. package/test/video-facts.test.js +0 -102
  285. package/test/viewer-assignments.test.js +0 -143
  286. package/test/viewer-outputs.test.js +0 -285
  287. package/test/viewer-presence.test.js +0 -116
  288. package/test/viewer-requests-admission.test.js +0 -92
  289. package/test/viewer-starts-on-map.test.js +0 -27
  290. package/test/viewer-states.test.js +0 -192
  291. package/test/viewer-subtitle-subscription.test.js +0 -96
  292. package/test/viewer.test.js +0 -139
  293. package/test/viewing-generations.test.js +0 -390
  294. package/test/visible-rung.test.js +0 -37
  295. package/test/wait-ledger.test.js +0 -100
  296. package/test/webrtc-shim.test.js +0 -149
  297. package/test/wedge-certainty.test.js +0 -131
  298. package/test/whole-file-held-read.test.js +0 -30
  299. package/test/withdraw-piece-claim.test.js +0 -212
  300. package/test/worker-channel.test.js +0 -158
@@ -1,221 +0,0 @@
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/torrent/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
- assert.deepEqual(
164
- matched.images,
165
- [],
166
- "a stray cover is not THIS film's cover: the one-video relaxation does not reach images, " +
167
- "because showing the wrong poster is a visible mistake where showing none is nothing"
168
- );
169
- });
170
-
171
- test("a contact sheet named by the video's whole name is paired with it", () => {
172
- // The shape a pack of a hundred videos actually ships: one sheet per video,
173
- // named `<video file name>.jpg`. Its BASE name is `c0930.com_chijyo0073.wmv`,
174
- // which no base-name rule would match against the video's `c0930…`.
175
- const files = [
176
- { path: "P/c0930.com_chijyo0073.wmv", name: "c0930.com_chijyo0073.wmv", length: 900 },
177
- {
178
- path: "P/Скринлисты/c0930.com_chijyo0073.wmv.jpg",
179
- name: "c0930.com_chijyo0073.wmv.jpg",
180
- length: 90
181
- },
182
- { path: "P/other.wmv", name: "other.wmv", length: 900 },
183
- { path: "P/Скринлисты/other.wmv.jpg", name: "other.wmv.jpg", length: 90 }
184
- ];
185
-
186
- const matched = matchSidecarFiles({ files, videoIndex: 0, torrentName: "P", videoCount: 2 });
187
-
188
- assert.equal(matched.images.length, 1, "its own sheet, and not the other video's");
189
- assert.equal(matched.images[0].fileIndex, 1);
190
- assert.deepEqual(matched.images[0].folders, ["Скринлисты"]);
191
- });
192
-
193
- test("a cover named like the film is paired with it, whatever its extension", () => {
194
- const files = [
195
- { path: "X/Film.2019.mkv", name: "Film.2019.mkv", length: 100 },
196
- { path: "X/Film.2019.png", name: "Film.2019.png", length: 10 },
197
- { path: "X/Film.2019.webp", name: "Film.2019.webp", length: 10 }
198
- ];
199
-
200
- const matched = matchSidecarFiles({ files, videoIndex: 0, torrentName: "X", videoCount: 1 });
201
-
202
- assert.deepEqual(
203
- matched.images.map((image) => image.name).sort(),
204
- ["Film.2019.png", "Film.2019.webp"]
205
- );
206
- });
207
-
208
- test("a raw elementary stream is recognised but declares no tracks of its own", () => {
209
- const files = [
210
- { path: "X/film.mkv", name: "film.mkv", length: 100 },
211
- { path: "X/film.ac3", name: "film.ac3", length: 100 }
212
- ];
213
- const matched = matchSidecarFiles({ files, videoIndex: 0, torrentName: "X", videoCount: 1 });
214
- assert.equal(matched.audio.length, 1);
215
- assert.equal(matched.audio[0].declaresTracks, false);
216
- });
217
-
218
- test("pictures are counted, and nothing else is", () => {
219
- assert.equal(countVideoFiles(driftersFiles(12)), 12);
220
- assert.equal(countVideoFiles([{ name: "a.mka" }, { name: "b.srt" }]), 0);
221
- });
@@ -1,255 +0,0 @@
1
- /**
2
- * @file The name grammar, checked against the forms its sources document.
3
- *
4
- * Every case here is an example one of the five surveyed players either prints
5
- * in its own documentation or handles in its own source. The survey and the
6
- * quotes are `research/sidecar-naming-conventions-2026-09-01.md`; the roadmap
7
- * item is 67.
8
- *
9
- * The rule these checks exist to hold: what the person who made the file wrote
10
- * is read, and nothing is guessed at. A token becomes a language only by being
11
- * in the table.
12
- */
13
-
14
- import test from "node:test";
15
- import assert from "node:assert/strict";
16
-
17
- import {
18
- isNeutralWord,
19
- languageFromFolderNames,
20
- lookupLanguage,
21
- nameFollows,
22
- parseLanguageTag,
23
- readSidecarName,
24
- sidecarNaming,
25
- tokenizeName
26
- } from "../services/torrent/naming.js";
27
-
28
- /**
29
- * Read a whole path against a known picture, the way the pairing does.
30
- *
31
- * `code` is `"und"` where nothing was read, which is the shape the browser is
32
- * given: a track that states no language is not a track with no language.
33
- *
34
- * @param {string} name - The sidecar's file name.
35
- * @param {string} [video] - The picture's file name.
36
- * @param {string} [relativePath] - The path, for the folders above the file.
37
- * @returns {{ code: string, group: string | null, isForced: boolean, isHearingImpaired: boolean, isDefault: boolean }}
38
- */
39
- function readPath(name, video = "Film.mkv", relativePath = name) {
40
- const folders = String(relativePath).split("/").slice(0, -1);
41
- const naming = sidecarNaming({ folders, fileName: name, videoName: video });
42
- return { ...naming, code: naming.code ?? "und", group: naming.releaser };
43
- }
44
-
45
- /** Read a name against a known picture, the way the loader does. */
46
- function read(name, video = "Film.mkv") {
47
- return readPath(name, video);
48
- }
49
-
50
- test("the dot-separated suffix, which is what every library writes", () => {
51
- // Plex: "Movie_Name (Release Date).[Language_Code].ext", the code being
52
- // "ISO-639-1 (2-letter) or ISO-639-2/B (3-letter)".
53
- assert.equal(read("Film.en.srt").code, "en");
54
- assert.equal(read("Film.eng.srt").code, "en");
55
- assert.equal(read("Film.ru.srt").code, "ru");
56
- assert.equal(read("Film.rus.srt").code, "ru");
57
- // Kodi's own example: "The Matrix.English.srt".
58
- assert.equal(read("Film.English.srt").code, "en");
59
- assert.equal(read("Film.Russian.srt").code, "ru");
60
- });
61
-
62
- test("both ISO 639-2 sets, because the two disagree for about twenty languages", () => {
63
- // Plex names the bibliographic set; ffmpeg and Matroska write either.
64
- for (const [bibliographic, terminological, code] of [
65
- ["ger", "deu", "de"], ["fre", "fra", "fr"], ["cze", "ces", "cs"],
66
- ["chi", "zho", "zh"], ["dut", "nld", "nl"], ["gre", "ell", "el"],
67
- ["per", "fas", "fa"], ["ice", "isl", "is"], ["may", "msa", "ms"],
68
- ["rum", "ron", "ro"], ["slo", "slk", "sk"], ["alb", "sqi", "sq"],
69
- ["arm", "hye", "hy"], ["baq", "eus", "eu"], ["bur", "mya", "my"],
70
- ["geo", "kat", "ka"], ["mac", "mkd", "mk"], ["mao", "mri", "mi"],
71
- ["tib", "bod", "bo"], ["wel", "cym", "cy"]
72
- ]) {
73
- assert.equal(lookupLanguage(bibliographic)?.code, code, bibliographic);
74
- assert.equal(lookupLanguage(terminological)?.code, code, terminological);
75
- }
76
- });
77
-
78
- test("a BCP-47 tag keeps its region, and Kodi's underscore form is accepted", () => {
79
- // Kodi: "a BCP 47 tag (with - characters replaced with _)"; its own example
80
- // is "The Matrix.zh_yue.srt".
81
- assert.deepEqual(parseLanguageTag("pt-BR"), { code: "pt-BR", name: "Portuguese" });
82
- assert.deepEqual(parseLanguageTag("pt_BR"), { code: "pt-BR", name: "Portuguese" });
83
- assert.deepEqual(parseLanguageTag("zh_yue"), { code: "zh-yue", name: "Chinese" });
84
- assert.deepEqual(parseLanguageTag("en-US"), { code: "en-US", name: "English" });
85
- assert.equal(read("Film.pt-BR.srt").code, "pt-BR");
86
- // A region is kept, so pt-BR and pt-PT stay apart on screen.
87
- assert.notEqual(read("Film.pt-BR.srt").code, read("Film.pt-PT.srt").code);
88
- });
89
-
90
- test("a tag whose primary subtag is not a language is not a language", () => {
91
- assert.equal(parseLanguageTag("web-dl"), null);
92
- assert.equal(parseLanguageTag("x-264"), null);
93
- assert.equal(parseLanguageTag("Upscale"), null);
94
- });
95
-
96
- test("the flags, with the spellings Jellyfin and mpv accept", () => {
97
- assert.equal(read("Film.en.forced.srt").isForced, true);
98
- assert.equal(read("Film.en.foreign.srt").isForced, true);
99
- assert.equal(read("Film.en.sdh.srt").isHearingImpaired, true);
100
- assert.equal(read("Film.en.cc.srt").isHearingImpaired, true);
101
- assert.equal(read("Film.default.srt").isDefault, true);
102
- // Jellyfin's own extended example: several flags on one file.
103
- const both = read("Film.default.en.forced.ass");
104
- assert.equal(both.code, "en");
105
- assert.equal(both.isDefault, true);
106
- assert.equal(both.isForced, true);
107
- });
108
-
109
- test("Kodi separates the forced flag with a dot, a space or a dash", () => {
110
- // "The forced flag can be either upper, lower or mixed case in the filename
111
- // and can use a '.' (dot), ' ' (space) or '-' (dash) to separate them".
112
- assert.equal(read("Film.English.Forced.srt").isForced, true);
113
- assert.equal(read("Film.English Forced.srt").isForced, true);
114
- assert.equal(read("Film.English-Forced.srt").isForced, true);
115
- assert.equal(read("Film.English Forced.srt").code, "en");
116
- });
117
-
118
- test("hi is Hindi alone and hearing-impaired beside another language", () => {
119
- // Jellyfin: "`hi` by itself will resolve as a Hindi language track, while
120
- // `hi` in conjunction with another language identifier … will use the other
121
- // language and tag it as hearing impaired."
122
- const hindi = read("Film.hi.srt");
123
- assert.equal(hindi.code, "hi");
124
- assert.equal(hindi.isHearingImpaired, false);
125
-
126
- const impaired = read("Film.en.hi.srt");
127
- assert.equal(impaired.code, "en");
128
- assert.equal(impaired.isHearingImpaired, true);
129
- });
130
-
131
- test("the last language in the name wins", () => {
132
- // Jellyfin: "If multiple languages are defined within the filename the last
133
- // one will be used and the others ignored."
134
- assert.equal(read("Film.en.ru.srt").code, "ru");
135
- assert.equal(read("Film.ru.en.srt").code, "en");
136
- });
137
-
138
- test("a bracketed tail is read, and only through the table", () => {
139
- // mpv takes `(` as the delimiter when the name ends `)`, `[` when it ends `]`.
140
- assert.equal(read("Film (Russian).srt").code, "ru");
141
- assert.equal(read("Film [rus].srt").code, "ru");
142
- // And the case from the test torrents that a shape-based rule would ruin.
143
- assert.equal(read("Gibo no Toiki - 01 (Upscale).ass").code, "und");
144
- assert.equal(read("[HorribleSubs] Drifters - 03 [1080p].ass").code, "und");
145
- // A bracketed tail does not stop the rest of the name being read.
146
- assert.equal(read("Film.rus [1080p].srt").code, "ru");
147
- assert.equal(read("Film.en.forced [x264].srt").code, "en");
148
- assert.equal(read("Film.en.forced [x264].srt").isForced, true);
149
- });
150
-
151
- test("leftover text is the track's title", () => {
152
- // Jellyfin: "Any arbitrary text not parsable to a language or flag will be
153
- // combined and used as the title of the stream."
154
- const info = read("Film.English Commentary.en.srt");
155
- assert.equal(info.code, "en");
156
- assert.equal(info.group, "English Commentary");
157
- // The underscore form this project already read, unchanged.
158
- assert.equal(read("Film_rus_AT_Team.srt").code, "ru");
159
- assert.equal(read("Film_rus_AT_Team.srt").group, "AT Team");
160
- });
161
-
162
- test("a leftover that only describes the encode is not a title", () => {
163
- // Jellyfin keeps every leftover word, and on a tidy library that is right.
164
- // A torrent is not tidy: this would read as "Russian (1080p)".
165
- assert.equal(read("Film.1080p.rus.srt").code, "ru");
166
- assert.equal(read("Film.1080p.rus.srt").group, null);
167
- assert.equal(read("Film.WEB-DL.x264.eng.srt").group, null);
168
- });
169
-
170
- test("the film's own name is never mined for a language", () => {
171
- // Jellyfin parses only what follows the video's name, and this is why: `It`
172
- // is Italian, and `Follows` is not a flag, so a reader that walked the whole
173
- // name would call this Italian.
174
- assert.equal(readPath("It.Follows.2014.srt", "It.Follows.2014.mkv").code, "und");
175
- // With no picture to measure against, reading stops at the first token that
176
- // is neither a flag nor a language — mpv's rule.
177
- assert.equal(readPath("It.Follows.2014.srt", "").code, "und");
178
- assert.equal(readPath("It.Follows.2014.rus.srt", "").code, "ru");
179
- });
180
-
181
- test("a folder states a language, or a kind of track, or a team", () => {
182
- assert.equal(languageFromFolderNames(["RUS"])?.code, "ru");
183
- assert.equal(languageFromFolderNames(["Rus Sub"])?.code, "ru");
184
- assert.equal(languageFromFolderNames(["subs", "eng"])?.code, "en");
185
- // VLC's own default search path — a place, not a language.
186
- for (const folder of ["Subs", "subs", "Subtitles", "subtitles"]) {
187
- assert.equal(languageFromFolderNames([folder]), null, folder);
188
- assert.equal(isNeutralWord(folder), true, folder);
189
- }
190
- // A team's bracket is not a language either.
191
- assert.equal(languageFromFolderNames(["Sub", "[Stan WarHammer & Nesitach]"]), null);
192
- });
193
-
194
- test("a Russian release names its folders in Russian", () => {
195
- assert.equal(languageFromFolderNames(["Субтитры"]), null);
196
- assert.equal(languageFromFolderNames(["Рус"])?.code, "ru");
197
- assert.equal(languageFromFolderNames(["Русские субтитры"])?.code, "ru");
198
- assert.equal(languageFromFolderNames(["Английские субтитры"])?.code, "en");
199
- assert.equal(languageFromFolderNames(["Рус Звук"])?.code, "ru");
200
- assert.equal(languageFromFolderNames(["Японские"])?.code, "ja");
201
- });
202
-
203
- test("the shapes the real test torrents carry still read as they did", () => {
204
- // Measured 2026-09-01 over the 112 torrents in Dropbox/trn.
205
- assert.equal(readPath("Enjo Kouhai_01.rus.ass", "Enjo Kouhai_01.mp4", "Enjo Kouhai/rus/Enjo Kouhai_01.rus.ass").code, "ru");
206
- assert.equal(readPath("Enjo Kouhai_01.chi.ass", "Enjo Kouhai_01.mp4", "Enjo Kouhai/chi/Enjo Kouhai_01.chi.ass").code, "zh");
207
- const drifters = readPath(
208
- "[HorribleSubs] Drifters - 03 [1080p].ass",
209
- "[HorribleSubs] Drifters - 03 [1080p].mkv",
210
- "Sub/[Stan WarHammer & Nesitach]/[HorribleSubs] Drifters - 03 [1080p].ass"
211
- );
212
- assert.equal(drifters.code, "und");
213
- assert.equal(drifters.group, "Stan WarHammer & Nesitach");
214
- });
215
-
216
- test("a soundtrack is read by the same grammar", () => {
217
- // Jellyfin's own example set: Film.en.ac3 beside Film.german.ac3.
218
- assert.equal(sidecarNaming({ folders: [], fileName: "Film.en.ac3", videoName: "Film.mkv" }).code, "en");
219
- assert.equal(sidecarNaming({ folders: [], fileName: "Film.german.ac3", videoName: "Film.mkv" }).code, "de");
220
- // The field case: the folder says Russian and the file carries only the
221
- // picture's own brackets.
222
- const dub = sidecarNaming({
223
- folders: ["Rus Sound"],
224
- fileName: "[HorribleSubs] Drifters - 02 [1080p].mka",
225
- videoName: "[HorribleSubs] Drifters - 02 [1080p].mkv"
226
- });
227
- assert.equal(dub.code, "ru");
228
- assert.equal(dub.releaser, null);
229
- });
230
-
231
- test("tokenizing is stable on names with nothing to say", () => {
232
- assert.deepEqual(tokenizeName(""), []);
233
- assert.deepEqual(readSidecarName("").language, null);
234
- assert.deepEqual(readSidecarName("Film", "Film").language, null);
235
- });
236
-
237
- test("a name continues the video's only at a token boundary", () => {
238
- // The rule that makes "begins with the video's name" safe. Without it
239
- // `Film.20.rus.srt` reads as belonging to `Film.2.mkv` — measured 2026-09-04
240
- // against the browser's own rule, which had no boundary — and the grammar
241
- // then reports the leftover `0` as the track's title, which is the tell that
242
- // the remainder is a fragment of another film's name.
243
- assert.equal(nameFollows("Film.2", "Film.2"), true, "the same name continues itself");
244
- assert.equal(nameFollows("Film.2.rus", "Film.2"), true);
245
- assert.equal(nameFollows("Film.20.rus", "Film.2"), false, "another film, one digit longer");
246
- assert.equal(nameFollows("Film_rus", "Film"), true);
247
- assert.equal(nameFollows("Film-rus", "Film"), true);
248
- assert.equal(nameFollows("Film rus", "Film"), true);
249
- // mpv's bracket forms.
250
- assert.equal(nameFollows("Film (Russian)", "Film"), true);
251
- assert.equal(nameFollows("Film [rus]", "Film"), true);
252
- // Nothing to measure against, and nothing to continue.
253
- assert.equal(nameFollows("Film.rus", ""), false);
254
- assert.equal(nameFollows("Fil", "Film"), false);
255
- });