Manifest fetch across the configured servers (JR-025 … JR-037)
🏗️ Build Plugin / build (push) Successful in 44s
Latest Release / latest-release (push) Successful in 40s
🧪 Test Plugin / test (push) Successful in 26s

Satisfies JRay-public-server UR-007. Servers are tried in configured order and
the first result clearing the configured tier wins; first-match rather than
best-match because querying every server for every item multiplies egress and
leaks the library to more parties, and the ordering already encodes which
source the admin prefers.

Every server is untrusted, including the pre-configured community one, so a
fetched manifest is re-validated against the same rules the server applies on
upload: envelope version refused if unknown, identifiers format-checked,
windows bounds-checked against the *local* file's runtime, belief bounded to
[0, 1], control and bidi characters refused in names. Responses are capped
while streaming rather than after buffering, since a hostile server can declare
any Content-Length it likes. HTTPS is required away from loopback. A failing
server is skipped with exponential backoff so one dead server cannot stall a
sweep.

The audio-tier offset is applied once, at store time, so stored truth is always
in the local file's own timebase and no read path needs offset awareness.
Windows are shifted, never reshaped — merging adjacent ones would answer "was a
face visible" rather than "was the actor present" (SR-002).

Also records why there is no `exact` tier, which was missing and led me to
re-add one. The file-hash tier is withdrawn on legal grounds: a TMDB id
discloses "some copy of this film", but an OpenSubtitles hash discloses "this
exact release", which turns a catalogue lookup into a release-identification
service and a server's database into a mapping from file fingerprints to the
instances holding them. The reason now lives on MatchTier and in SPEC.md §JR-036,
`TitleQuery` has no VideoHash property so there is nothing to send, and a test
asserts the enum has no Exact member — the spec had still listed `exact` as a
configurable tier, which is what made the removal look like an oversight.

42 tests.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

TRACES: JR-025, JR-027, JR-028, JR-029, JR-030, JR-031, JR-036, JR-037 | PR-005, PR-006
This commit is contained in:
2026-07-31 10:03:49 +02:00
co-authored by Claude Opus 5
parent 64f549ab35
commit 3d210b5bd3
23 changed files with 1858 additions and 18 deletions
@@ -23,6 +23,28 @@ public enum ServerTrustLevel
/// The minimum cut-match tier a fetched manifest must reach before it is stored.
/// See the public server specification, §3 "Cut matching".
/// </summary>
/// <remarks>
/// Every tier is a claim about a <em>cut</em>, never about a copy. There is no
/// file-level tier, and the plugin sends no <c>video_hash</c>.
/// <para>
/// <b>The <c>Exact</c> tier was withdrawn for legal reasons; do not re-add it
/// without an explicit, recorded agreement.</b> It keyed on the OpenSubtitles
/// file hash, which identifies the individual encode a user holds rather than
/// the edit the timings describe. A TMDB id discloses "some copy of this film";
/// a file hash discloses "<em>this exact release</em>", which turns a catalogue
/// lookup into a release-identification service and turns the server's database
/// into a mapping from file fingerprints to the instances holding them. That is
/// a far more specific disclosure than PR-005 permits, and a dataset no
/// volunteer operator should be holding.
/// </para>
/// <para>
/// The audio signature is the deliberate replacement: derived from content, it
/// identifies the <em>cut</em>, so two different encodes of the same edit agree.
/// It answers the question the exchange needs — "do these timings apply to this
/// media?" — without answering the one it must not. <c>Audio</c> is therefore
/// the top tier here.
/// </para>
/// </remarks>
public enum MatchTier
{
/// <summary>Audio 0.600.85, or runtimes within ±30s. Surfaced as a caveat in the UI.</summary>
@@ -31,11 +53,8 @@ public enum MatchTier
/// <summary>Runtimes within ±2s.</summary>
Runtime = 1,
/// <summary>Audio signature score ≥ 0.85; may carry a non-zero offset.</summary>
/// <summary>Audio signature score ≥ 0.85; may carry a non-zero offset. The top tier.</summary>
Audio = 2,
/// <summary>Identical <c>video_hash</c> — the same file.</summary>
Exact = 3,
}
/// <summary>