@torrent-tv/proxy 2.88.4 → 2.88.5
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/.claude/commands/opsx/apply.md +155 -155
- package/.claude/commands/opsx/archive.md +160 -160
- package/.claude/commands/opsx/explore.md +174 -174
- package/.claude/commands/opsx/propose.md +109 -109
- package/.claude/commands/opsx/sync.md +143 -143
- package/.claude/skills/openspec-apply-change/SKILL.md +159 -159
- package/.claude/skills/openspec-archive-change/SKILL.md +117 -117
- package/.claude/skills/openspec-explore/SKILL.md +289 -289
- package/.claude/skills/openspec-propose/SKILL.md +113 -113
- package/.claude/skills/openspec-sync-specs/SKILL.md +147 -147
- package/.dockerignore +5 -5
- package/AGENTS.md +322 -322
- package/CHANGELOG.md +2266 -2259
- package/Dockerfile +30 -30
- package/LICENSE +16 -16
- package/README.md +369 -369
- package/assets/calibration/NOTICE.md +100 -100
- package/assets/calibration/audio/NOTICE.md +19 -0
- package/assets/calibration/audio/aac.m4a +0 -0
- package/assets/calibration/audio/ac3.ac3 +0 -0
- package/assets/calibration/audio/alac.m4a +0 -0
- package/assets/calibration/audio/dts.dts +0 -0
- package/assets/calibration/audio/eac3.eac3 +0 -0
- package/assets/calibration/audio/flac.flac +0 -0
- package/assets/calibration/audio/manifest.json +98 -0
- package/assets/calibration/audio/mp2.mp2 +0 -0
- package/assets/calibration/audio/mp3.mp3 +0 -0
- package/assets/calibration/audio/opus.opus +0 -0
- package/assets/calibration/audio/pcm.wav +0 -0
- package/assets/calibration/audio/truehd.thd +0 -0
- package/assets/calibration/audio/vorbis.ogg +0 -0
- package/assets/diagnostics/sctpstate.gdb +31 -31
- package/bin/cli.js +684 -684
- package/bin/thread-pool.js +30 -30
- package/biome.json +412 -411
- package/docs/browser-proxy-contract.md +155 -152
- package/docs/container-architecture.md +218 -218
- package/docs/download-architecture.md +261 -261
- package/docs/encode-architecture.md +729 -729
- package/docs/encode-run-state.md +96 -96
- package/docs/logs.md +97 -97
- package/docs/playback-start-forecast.md +56 -0
- package/docs/socks5-egress-research.md +114 -114
- package/docs/storage-architecture.md +267 -267
- package/knip.json +14 -14
- package/openspec/changes/adaptive-bitrate/design.md +92 -92
- package/openspec/changes/adaptive-bitrate/proposal.md +56 -56
- package/openspec/changes/adaptive-bitrate/specs/adaptive-bitrate/spec.md +51 -51
- package/openspec/changes/adaptive-bitrate/tasks.md +40 -40
- package/openspec/changes/binary-distribution/.openspec.yaml +2 -2
- package/openspec/changes/binary-distribution/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/design.md +81 -81
- package/openspec/changes/chunked-request-bodies/proposal.md +53 -53
- package/openspec/changes/chunked-request-bodies/specs/chunked-request-bodies/spec.md +30 -30
- package/openspec/changes/chunked-request-bodies/tasks.md +40 -40
- package/openspec/changes/cold-start/design.md +98 -98
- package/openspec/changes/cold-start/proposal.md +62 -62
- package/openspec/changes/cold-start/specs/cold-start/spec.md +47 -47
- package/openspec/changes/cold-start/tasks.md +48 -48
- package/openspec/changes/disk-cap/.openspec.yaml +2 -2
- package/openspec/changes/disk-cap/proposal.md +37 -37
- package/openspec/changes/disk-cap/specs/disk-cap/spec.md +25 -25
- package/openspec/changes/disk-cap/tasks.md +19 -19
- package/openspec/changes/proxy-observability/.openspec.yaml +2 -2
- package/openspec/changes/proxy-observability/design.md +38 -38
- package/openspec/changes/proxy-observability/proposal.md +49 -49
- package/openspec/changes/proxy-observability/specs/observability/spec.md +33 -33
- package/openspec/changes/proxy-observability/tasks.md +21 -21
- package/openspec/changes/seek-debounce/design.md +118 -118
- package/openspec/changes/seek-debounce/proposal.md +59 -59
- package/openspec/changes/seek-debounce/specs/seek-debounce/spec.md +41 -41
- package/openspec/changes/seek-debounce/tasks.md +33 -33
- package/openspec/changes/subtitle-language/.openspec.yaml +2 -2
- package/openspec/changes/subtitle-language/proposal.md +50 -50
- package/openspec/changes/subtitle-language/specs/subtitle-language/spec.md +34 -34
- package/openspec/changes/subtitle-language/tasks.md +26 -26
- package/openspec/changes/track-selection/.openspec.yaml +2 -2
- package/openspec/changes/track-selection/design.md +47 -47
- package/openspec/changes/track-selection/proposal.md +45 -45
- package/openspec/changes/track-selection/specs/track-selection/spec.md +41 -41
- package/openspec/changes/track-selection/tasks.md +20 -20
- package/openspec/changes/transcode-quality/.openspec.yaml +2 -2
- package/openspec/changes/transcode-quality/proposal.md +51 -51
- package/openspec/changes/transcode-quality/specs/transcode-quality/spec.md +139 -139
- package/openspec/changes/transcode-quality/tasks.md +63 -63
- package/openspec/config.yaml +38 -38
- package/package.json +1 -1
- package/public/.well-known/appspecific/com.chrome.devtools.json +1 -1
- package/research/delivery-wedge-stand-ladder-2026-09-13.md +329 -329
- package/research/double-spawn-2026-09-10.md +171 -171
- package/research/handover-2026-09-13.md +209 -209
- package/research/handover-reader-claims-removal-2026-09-12.md +166 -166
- package/research/piece-withdrawn-but-still-claimed-2026-09-12.md +175 -175
- package/research/preset-benchmark-size-scaling-2026-08-22.md +55 -55
- package/research/priority-map-is-the-truth-2026-09-12.md +225 -225
- package/routes/api/client-logs/post.js +86 -86
- package/routes/api/delivery-sink/get.js +72 -72
- package/routes/api/link-probe/get.js +51 -51
- package/routes/api/playback-plan/audio-tracks/post.js +29 -29
- package/routes/api/playback-plan/post.js +53 -53
- package/routes/api/sources/post.js +36 -36
- package/routes/api/sources/stats/get.js +132 -132
- package/routes/api/sources/warm/post.js +217 -217
- package/routes/api/subtitles/get.js +230 -230
- package/routes/api/transcode-sessions/fragment-far/post.js +60 -60
- package/routes/api/transcode-sessions/net-report/post.js +138 -138
- package/routes/api/transcode-sessions/post.js +178 -178
- package/routes/api/transcode-sessions/progress/get.js +27 -27
- package/routes/api/transcode-sessions/release/post.js +41 -41
- package/routes/api/transcode-sessions/seek/post.js +63 -63
- package/routes/health/get.js +13 -13
- package/routes/healthz/get.js +13 -13
- package/routes/stream/get.js +380 -380
- package/routes/transcode/audio-file/get.js +64 -64
- package/routes/transcode/audio-warm/get.js +95 -95
- package/routes/transcode/session-file/get.js +52 -52
- package/routes/transcode/variant-file/get.js +95 -95
- package/routes/transcode/variant-warm/get.js +84 -84
- package/scripts/build-audio-samples.mjs +31 -0
- package/scripts/render-run-graph.js +173 -173
- package/server.js +730 -725
- package/services/encode/CoverageMap.js +428 -428
- package/services/encode/CushionReport.js +345 -345
- package/services/encode/EncodeAdmission.js +376 -376
- package/services/encode/EncodeOrchestrator.js +1217 -1217
- package/services/encode/EncodePlan.js +1366 -1366
- package/services/encode/EncodeRun.js +813 -813
- package/services/encode/EncodeRuns.js +1237 -1235
- package/services/encode/Encoder.js +142 -142
- package/services/encode/EncoderSelection.js +38 -38
- package/services/encode/LocalObservations.js +359 -359
- package/services/encode/NvencEncoder.js +50 -50
- package/services/encode/OutputOpening.js +783 -783
- package/services/encode/OutputTimes.js +579 -579
- package/services/encode/QsvEncoder.js +58 -58
- package/services/encode/Renditions.js +2282 -2282
- package/services/encode/RunProgress.js +84 -84
- package/services/encode/SegmentDemand.js +95 -95
- package/services/encode/SoftwareEncoder.js +121 -121
- package/services/encode/V4l2m2mEncoder.js +53 -53
- package/services/encode/VaapiEncoder.js +66 -66
- package/services/encode/args.js +418 -418
- package/services/encode/audio-calibration.js +67 -0
- package/services/encode/audio-work-rate.js +15 -0
- package/services/encode/available-share.js +89 -89
- package/services/encode/calibration.js +361 -361
- package/services/encode/contention.js +128 -128
- package/services/encode/decode-cost-fit.js +363 -363
- package/services/encode/encode-exit.js +114 -114
- package/services/encode/encode-run-state.js +551 -551
- package/services/encode/encoder-readings.js +45 -45
- package/services/encode/fingerprint.js +322 -322
- package/services/encode/host-load.js +174 -174
- package/services/encode/hwaccel.js +1894 -1894
- package/services/encode/index.js +9 -9
- package/services/encode/learned-median.js +90 -90
- package/services/encode/output/EncodedOutput.js +57 -57
- package/services/encode/output/Output.js +131 -131
- package/services/encode/output/OutputCatalog.js +414 -414
- package/services/encode/output/OutputSpec.js +390 -390
- package/services/encode/output/Timeline.js +308 -308
- package/services/encode/output/cut-grid.js +107 -107
- package/services/encode/output/index.js +20 -20
- package/services/encode/output/ladder.js +32 -32
- package/services/encode/output/playlists.js +284 -284
- package/services/encode/output/rates.js +125 -125
- package/services/encode/output-key-format.js +21 -21
- package/services/encode/quality/EncodeCost.js +1167 -1150
- package/services/encode/quality/HostLoad.js +525 -525
- package/services/encode/quality/HostTimings.js +249 -249
- package/services/encode/quality/OutputQualityState.js +28 -28
- package/services/encode/quality/QualityController.js +935 -935
- package/services/encode/quality/QualityOffer.js +416 -416
- package/services/encode/quality/drain-threat.js +47 -47
- package/services/encode/quality/link-budget.js +349 -349
- package/services/encode/quality/output-format.js +319 -319
- package/services/encode/quality/serving-output.js +216 -216
- package/services/encode/quality/visible-rung.js +57 -57
- package/services/encode/run-budget.js +97 -97
- package/services/encode/run-command.js +754 -754
- package/services/encode/run-costs.js +155 -155
- package/services/encode/run-inputs.js +42 -42
- package/services/encode/segment-formats/fmp4.js +458 -456
- package/services/encode/segment-formats/index.js +85 -85
- package/services/encode/segment-formats/init-compat.js +324 -324
- package/services/encode/segment-formats/mp4-boxes.js +12 -1
- package/services/encode/segment-formats/mpegts.js +134 -134
- package/services/encode/start-stop-cost.js +178 -178
- package/services/encode/throughput.js +97 -97
- package/services/encode/torrent-cost.js +88 -88
- package/services/identity/installation-id.js +38 -38
- package/services/media/ContainerOrchestrator.js +149 -149
- package/services/media/KeyframeTables.js +316 -316
- package/services/media/SourceFile.js +298 -298
- package/services/media/SubtitleCues.js +520 -520
- package/services/media/SubtitleOrchestrator.js +167 -167
- package/services/media/audio-inventory.js +299 -298
- package/services/media/container/AviContainer.js +266 -266
- package/services/media/container/Container.js +609 -609
- package/services/media/container/ContainerFactory.js +56 -56
- package/services/media/container/KeyframeTable.js +272 -272
- package/services/media/container/MatroskaContainer.js +1494 -1494
- package/services/media/container/Mp4Container.js +1344 -1344
- package/services/media/container/ebml-reader.js +159 -159
- package/services/media/ffmpeg-banner.js +260 -260
- package/services/media/keyframe-probe.js +157 -157
- package/services/media/media-info-probe.js +93 -93
- package/services/media/playback-planner.js +931 -931
- package/services/media/tracks/AudioTrack.js +143 -143
- package/services/media/tracks/ContainerTrack.js +75 -75
- package/services/media/tracks/ImageSubtitleTrack.js +19 -19
- package/services/media/tracks/SubtitleTrack.js +38 -38
- package/services/media/tracks/TextSubtitleTrack.js +287 -287
- package/services/media/tracks/VideoTrack.js +33 -33
- package/services/media/tracks/audio-bound.js +233 -233
- package/services/media/tracks/index.js +15 -15
- package/services/media/tracks/language-detect.js +174 -174
- package/services/server/OutputLifecycle.js +418 -418
- package/services/server/SegmentServing.js +1150 -1150
- package/services/server/ViewerRequests.js +785 -760
- package/services/server/WaitLedger.js +142 -142
- package/services/server/controllers/PlaybackController.js +40 -40
- package/services/server/controllers/SubtitleController.js +109 -109
- package/services/server/http-responses.js +60 -60
- package/services/server/playback-readiness.js +516 -708
- package/services/server/transcode-session-files.js +325 -325
- package/services/server/wire-outputs.js +1020 -995
- package/services/storage/Diagnostics.js +202 -202
- package/services/storage/MachineBudget.js +235 -235
- package/services/storage/allowance.js +107 -107
- package/services/storage/core-dumps.js +106 -106
- package/services/storage/files/CompletedFiles.js +391 -391
- package/services/storage/files/piece-from-whole-file.js +118 -118
- package/services/storage/free.js +71 -71
- package/services/storage/keep.js +48 -48
- package/services/storage/machine-memory.js +56 -56
- package/services/storage/memory-report.js +575 -575
- package/services/storage/piece-store/piece-disk-store.js +539 -539
- package/services/storage/piece-store/piece-lru.js +495 -495
- package/services/storage/piece-store/shared-piece-store.js +1937 -1931
- package/services/storage/returns.js +103 -103
- package/services/storage/segment-store/SegmentStore.js +1255 -1240
- package/services/storage/wire.js +175 -175
- package/services/torrent/demand/DemandRegister.js +202 -202
- package/services/torrent/demand/Urgency.js +137 -137
- package/services/torrent/demand/Window.js +118 -118
- package/services/torrent/demand/index.js +8 -8
- package/services/torrent/demand/pieces.js +140 -140
- package/services/torrent/download/SwarmSelection.js +386 -386
- package/services/torrent/download/registry.js +102 -102
- package/services/torrent/download/withdraw-claim.js +80 -80
- package/services/torrent/files.js +455 -455
- package/services/torrent/naming.js +619 -619
- package/services/torrent/piece-store-of.js +29 -29
- package/services/torrent/supply-margin.js +262 -262
- package/services/torrent/torrent-pool.js +2688 -2675
- package/services/torrent/worker/background-fill.js +213 -213
- package/services/torrent/worker/channel.js +271 -271
- package/services/torrent/worker/client.js +765 -765
- package/services/torrent/worker/fastest-wires.js +342 -342
- package/services/torrent/worker/handle-state.js +23 -23
- package/services/torrent/worker/held-bytes.js +155 -155
- package/services/torrent/worker/install-webrtc-shim.js +32 -32
- package/services/torrent/worker/piece-reader.js +1078 -1078
- package/services/torrent/worker/pool-adapter.js +371 -371
- package/services/torrent/worker/protocol.js +184 -184
- package/services/torrent/worker/resume-warm.js +107 -107
- package/services/torrent/worker/webrtc-shim.js +219 -219
- package/services/torrent/worker/worker.js +1129 -1129
- package/services/transport/data-channel-handler.js +1369 -1369
- package/services/transport/delivery-probe.js +842 -842
- package/services/transport/health-collector.js +53 -53
- package/services/transport/nat-classifier.js +231 -231
- package/services/transport/packet-witness.js +808 -808
- package/services/transport/port-mapper.js +235 -235
- package/services/transport/registry-api.js +81 -81
- package/services/transport/tunnel-client.js +447 -447
- package/services/transport/usrsctp-state.js +175 -175
- package/services/transport/webrtc-manager.js +484 -484
- package/services/viewer/Assignments.js +340 -340
- package/services/viewer/PriorityMap.js +390 -390
- package/services/viewer/PriorityOrchestrator.js +325 -325
- package/services/viewer/Viewer.js +691 -691
- package/services/viewer/Viewers.js +368 -368
- package/services/viewer/active-output.js +37 -37
- package/services/viewer/audio-start.js +46 -46
- package/services/viewer/buffer-trend.js +81 -81
- package/services/viewer/choices.js +647 -647
- package/services/viewer/positions.js +126 -126
- package/services/viewer/report-intake.js +46 -46
- package/store/source-registry.js +71 -71
- package/test/asset-paths.test.js +28 -28
- package/test/audio-calibration.test.js +23 -0
- package/test/audio-inventory.test.js +212 -212
- package/test/audio-track-bitrate.test.js +40 -40
- package/test/auto-quality-step.test.js +707 -707
- package/test/available-share.test.js +76 -76
- package/test/background-fill.test.js +190 -190
- package/test/client-log-file.test.js +110 -110
- package/test/completed-files.test.js +115 -115
- package/test/concurrent-cost.test.js +148 -148
- package/test/container-media-info.test.js +228 -228
- package/test/contention.test.js +76 -76
- package/test/core-dumps.test.js +57 -57
- package/test/coverage-follows-the-disk.test.js +228 -228
- package/test/coverage-map.test.js +195 -195
- package/test/cue-push-carries-what-the-walk-found.test.js +58 -58
- package/test/cut-times-timeline.test.js +65 -65
- package/test/cuts-follow-published-grid.test.js +109 -109
- package/test/data-channel-frame.test.js +63 -63
- package/test/data-channel-viewer-lifecycle.test.js +92 -92
- package/test/dead-torrent-handle.test.js +52 -52
- package/test/declared-rates.test.js +116 -116
- package/test/declared-tracks.test.js +43 -43
- package/test/decode-cost-fit.test.js +202 -202
- package/test/decode-cost.test.js +462 -462
- package/test/delivery-probe.test.js +393 -393
- package/test/demand-register.test.js +206 -206
- package/test/dht-bootstrap.test.js +72 -72
- package/test/drain-threat.test.js +65 -65
- package/test/duplicate-add.test.js +48 -48
- package/test/encode-admission.test.js +146 -146
- package/test/encode-cost.test.js +283 -283
- package/test/encode-exit.test.js +111 -111
- package/test/encode-orchestrator.test.js +419 -419
- package/test/encode-plan-viewers.test.js +718 -718
- package/test/encode-plan.test.js +545 -545
- package/test/encode-run-state.test.js +314 -314
- package/test/encode-run.test.js +162 -162
- package/test/encode-slope.test.js +46 -46
- package/test/encoded-output.test.js +39 -39
- package/test/encoder-kinds.test.js +122 -122
- package/test/encoder-readings.test.js +46 -46
- package/test/every-claimant-is-told-its-share.test.js +152 -152
- package/test/failed-encoder-still-serves-what-exists.test.js +354 -354
- package/test/failed-start-breaker.test.js +202 -202
- package/test/fastest-wires.test.js +143 -143
- package/test/ffmpeg-stream-counts.test.js +78 -78
- package/test/file-edges.test.js +191 -191
- package/test/fmp4-presentation-clock.test.js +162 -147
- package/test/fmp4-self-contained.test.js +80 -80
- package/test/fmp4-timestamps.test.js +140 -140
- package/test/fmp4-track-completeness.test.js +41 -41
- package/test/format-keyed-outputs.test.js +78 -78
- package/test/health-metrics.test.js +38 -38
- package/test/held-request-width.test.js +157 -157
- package/test/helpers/encode-run.js +136 -136
- package/test/helpers/held-file.js +29 -29
- package/test/helpers/manager.js +57 -57
- package/test/helpers/output-spec.js +63 -63
- package/test/host-load.test.js +68 -68
- package/test/index-accuracy-is-about-the-file.test.js +128 -128
- package/test/init-compat.test.js +219 -219
- package/test/input-lost-quiets-the-plan.test.js +261 -261
- package/test/input-unavailable.test.js +48 -48
- package/test/installation-id.test.js +39 -39
- package/test/keeping-period.test.js +83 -83
- package/test/keyframe-index-accuracy.test.js +188 -188
- package/test/keyframe-table-budget.test.js +102 -102
- package/test/keyframes-belong-to-the-file.test.js +249 -249
- package/test/learned-median.test.js +37 -37
- package/test/letting-go-asks-who-is-watching.test.js +191 -191
- package/test/limit-row-by-area.test.js +60 -60
- package/test/link-budget.test.js +112 -112
- package/test/link-probe.test.js +56 -56
- package/test/local-encoding-data.test.js +273 -273
- package/test/logger-repeats.test.js +134 -134
- package/test/look-ahead-coverage.test.js +56 -56
- package/test/manager-surface.test.js +292 -292
- package/test/matroska-cues-track.test.js +192 -192
- package/test/memory-budget.test.js +390 -390
- package/test/move-cost.test.js +216 -216
- package/test/mp4-subtitles.test.js +173 -173
- package/test/one-authority.test.js +285 -285
- package/test/one-budget.test.js +149 -149
- package/test/one-output-one-session.test.js +273 -270
- package/test/one-resource-divided.test.js +185 -185
- package/test/one-serving-output-per-height.test.js +458 -458
- package/test/orchestrator-wired.test.js +206 -206
- package/test/output-catalog.test.js +39 -39
- package/test/output-shape.test.js +21 -21
- package/test/output-spec.test.js +251 -251
- package/test/output-speed.test.js +88 -88
- package/test/outputs-being-prepared.test.js +40 -40
- package/test/packet-witness-ring.test.js +236 -236
- package/test/packet-witness.test.js +148 -148
- package/test/piece-disk-store.test.js +401 -401
- package/test/piece-from-whole-file.test.js +129 -129
- package/test/piece-lru.test.js +319 -319
- package/test/piece-reader.test.js +162 -162
- package/test/piece-selection-offset.test.js +85 -85
- package/test/piece-store-eviction.test.js +595 -595
- package/test/piece-store-never-refuses.test.js +153 -153
- package/test/piece-store-reservations.test.js +403 -403
- package/test/piece-store-slow-disk.test.js +159 -159
- package/test/piece-tail.test.js +97 -97
- package/test/plan-host-timings.test.js +68 -68
- package/test/plan-is-steady.test.js +225 -225
- package/test/playback-readiness.test.js +483 -453
- package/test/playback-start-regressions.test.js +99 -99
- package/test/priority-map-download.test.js +174 -174
- package/test/priority-map-emptied.test.js +204 -204
- package/test/priority-map-per-output.test.js +215 -215
- package/test/priority-map.test.js +163 -163
- package/test/probe-wedge-certainty.test.js +60 -60
- package/test/produced-copy-choice.test.js +213 -213
- package/test/published-timeline.test.js +83 -83
- package/test/quality-offer.test.js +162 -162
- package/test/quality-variants.test.js +1257 -1257
- package/test/rate-control-in-the-output.test.js +180 -180
- package/test/read-survives-withdrawal.test.js +164 -164
- package/test/read-window.test.js +365 -365
- package/test/resume-warm.test.js +39 -39
- package/test/run-graph-drift.test.js +37 -37
- package/test/run-inputs.test.js +80 -80
- package/test/run-intervals.test.js +100 -100
- package/test/run-position-follows-published-grid.test.js +89 -89
- package/test/run-progress.test.js +65 -65
- package/test/same-height-switch.test.js +664 -664
- package/test/seek-frees-the-viewers-requests.test.js +217 -217
- package/test/seek-landing.test.js +109 -109
- package/test/seek-target-not-superseded.test.js +195 -195
- package/test/segment-closed-on-both-branches.test.js +96 -96
- package/test/segment-cut-times.test.js +63 -63
- package/test/segment-demand.test.js +80 -80
- package/test/segment-hold-supersede.test.js +91 -91
- package/test/segment-judged-by-its-own-header.test.js +221 -221
- package/test/segment-serve-wiring.test.js +475 -475
- package/test/segment-store-eviction.test.js +247 -247
- package/test/segment-store.test.js +338 -323
- package/test/segment-true-start.test.js +86 -86
- package/test/segments-are-shared.test.js +176 -176
- package/test/send-chunk-bytes.test.js +86 -86
- package/test/serving-existing-output.test.js +105 -105
- package/test/serving-output.test.js +102 -102
- package/test/shared-piece-store.test.js +310 -310
- package/test/sidecar-naming.test.js +142 -142
- package/test/soundtrack-rate-bound.test.js +289 -289
- package/test/source-file.test.js +133 -133
- package/test/source-registry.test.js +18 -18
- package/test/split-a-stretch.test.js +139 -139
- package/test/stale-request-after-seek.test.js +158 -158
- package/test/startup-readings.test.js +124 -124
- package/test/stream-route.test.js +209 -209
- package/test/subtitle-cue-framing.test.js +200 -200
- package/test/subtitle-cue-source.test.js +102 -102
- package/test/subtitle-cue-walk.test.js +370 -370
- package/test/subtitle-cursor.test.js +63 -63
- package/test/subtitle-defaults.test.js +118 -118
- package/test/subtitle-language.test.js +252 -252
- package/test/subtitle-track-numbering.test.js +386 -386
- package/test/supply-margin.test.js +177 -177
- package/test/swarm-follows-readers.test.js +174 -174
- package/test/swarm-reach.test.js +130 -130
- package/test/swarm-selection.test.js +220 -220
- package/test/tail-duplication.test.js +167 -167
- package/test/the-channel-has-both-ends.test.js +91 -91
- package/test/the-torrent-thread-serves-bytes.test.js +107 -107
- package/test/thread-pool.test.js +53 -53
- package/test/timeline.test.js +84 -84
- package/test/torrent-contents.test.js +235 -235
- package/test/torrent-cost.test.js +71 -71
- package/test/torrent-files.test.js +221 -221
- package/test/torrent-naming.test.js +255 -255
- package/test/tracks-begin-together.test.js +209 -209
- package/test/tunnel-renewal.test.js +167 -167
- package/test/two-viewers-one-picture.test.js +504 -504
- package/test/upload-hurry.test.js +186 -186
- package/test/usrsctp-state.test.js +131 -131
- package/test/video-facts.test.js +102 -102
- package/test/viewer-assignments.test.js +143 -143
- package/test/viewer-outputs.test.js +285 -285
- package/test/viewer-presence.test.js +116 -116
- package/test/viewer-requests-admission.test.js +89 -89
- package/test/viewer-states.test.js +189 -189
- package/test/viewer-subtitle-subscription.test.js +96 -96
- package/test/viewer.test.js +139 -139
- package/test/viewing-generations.test.js +390 -390
- package/test/visible-rung.test.js +37 -37
- package/test/wait-ledger.test.js +100 -100
- package/test/webrtc-shim.test.js +149 -149
- package/test/wedge-certainty.test.js +131 -131
- package/test/withdraw-piece-claim.test.js +212 -212
- package/test/worker-channel.test.js +158 -158
- package/utils/client-log-file.js +151 -151
- package/utils/logger.js +312 -312
- package/utils/parse-range.js +25 -25
- package/utils/perf.js +59 -59
- package/utils/torrent-source-key.js +38 -38
|
@@ -1,255 +1,255 @@
|
|
|
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
|
-
});
|
|
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
|
+
});
|