feat(audio): align a fetched manifest to the local file before storing
The signature had a producer and a reader but no consumer, so nothing ever fingerprinted anything. `ManifestAligner` runs on the fetch path, before the windows are stored. A local alignment supersedes the server's offset. The server has never seen this file — its offset is a runtime-difference inference at best, while the local comparison is against the media the windows will actually be drawn over. It also needs no round trip, so no signature leaves the instance. This is what jRay's spec already meant by matching being a consumer concern: the server never rewrites a manifest, so one stored manifest serves every trim of the same cut. The offset has two terms and only one is in the server's pseudocode. Both windows are centred on their own file's midpoint, so unequal runtimes start them at different absolute times; a release with 40 s of extra head material recovers 20 s from the slide and 20 s from the anchor difference. Using the slide alone is wrong by half the runtime difference on every shifted release. Degradation, never failure. Signatures off, no manifest signature, media under the window, a `v2:` producer, a missing binary, a decode error — each applies the server's offset rather than refusing, because a signature is an enhancement to cut matching and must never break a fetch. "Un-comparable" and "does not match" are kept distinct, which a test caught: `Compare` returns null for both, and conflating them would report a 90-second extra as content disagreeing with its own manifest. A genuine disagreement is stored anyway — the audio may legitimately differ, a different language track being the obvious case — and surfaced as a caveat that outranks the tier's, since it is the stronger statement. The applied offset, score, slide and the local file's own signature are written beside the truth file: the offset is otherwise unrecoverable once the windows are shifted, and the stored signature lets a later fetch align without decoding again. Provenance is never injected into the truth file, so the bytes served back stay the producer's (JR-004). `docs/audio-alignment.md` documents the mechanism end to end. TRACES: JR-047 | SR-003
This commit is contained in:
@@ -0,0 +1,288 @@
|
||||
# Audio signature and alignment — how it works
|
||||
|
||||
How a manifest fetched from a public server is checked against, and shifted onto,
|
||||
your own copy of a film.
|
||||
|
||||
Requirements: `JR-042` … `JR-045`, `JR-047`. Construction is owned by
|
||||
[`JRay-public-server/SPEC.md` §3](../../JRay-public-server/SPEC.md); this document
|
||||
is the mechanism end to end.
|
||||
|
||||
---
|
||||
|
||||
## The problem
|
||||
|
||||
A manifest says "Peter Capaldi is on screen from 42:10 to 42:38". Your file may
|
||||
not agree, because releases of the same film are trimmed differently — a
|
||||
distributor logo here, a longer certificate card there. Thirty seconds of extra
|
||||
head material makes every window in the manifest wrong by thirty seconds.
|
||||
|
||||
The old defence was the runtime tier: accept a manifest only if the runtimes
|
||||
agree within ±2 s. That rejects exactly the releases it should be fixing, and
|
||||
accepts anything that happens to be the same length.
|
||||
|
||||
A **content-derived signature** answers the question the exchange actually needs
|
||||
— *do these timings apply to this media?* — and, when the answer is "yes but
|
||||
shifted", says by how much.
|
||||
|
||||
Deliberately, it does not answer *what file is this?* There is no file hash
|
||||
anywhere in the system; the tier was withdrawn on legal grounds. The signature
|
||||
identifies a **cut**, so two different encodes of the same edit agree.
|
||||
|
||||
---
|
||||
|
||||
## The signature
|
||||
|
||||
120 seconds from the **centre** of the media — the head and tail are the least
|
||||
content-specific parts of a release, being logos and credits.
|
||||
|
||||
```
|
||||
decode centre window runtime/2 ± 60 s
|
||||
downmix mono
|
||||
resample 11025 Hz
|
||||
STFT 4096-sample frame, 1024-sample hop (~93 ms), Hann
|
||||
band 300–3000 Hz, 32 logarithmically spaced bands
|
||||
per frame peak band index (5 bits) + energy class (2 bits)
|
||||
-> one byte, bit 7 always clear
|
||||
result 1288 bytes, base64, prefixed "v1:"
|
||||
```
|
||||
|
||||
Peak bins are used because they survive lossy re-encoding, loudness
|
||||
normalisation and channel-layout differences, where absolute magnitudes do not.
|
||||
Same principle as Chromaprint/AcoustID, but self-contained: no external service
|
||||
is queried, so no lookup leaks which titles an instance holds.
|
||||
|
||||
Bit 7 being always clear is not decoration. It is what makes an arbitrary byte
|
||||
string *not* a valid signature, which is what the server validates on upload, and
|
||||
what stops the field being usable as a payload channel. The length is fixed by
|
||||
the construction rather than merely bounded, for the same reason — a caller
|
||||
cannot choose it, so it cannot become a variable-size container.
|
||||
|
||||
### Why the parameters are written down twice
|
||||
|
||||
The specification's prose does not determine a byte stream. Two people
|
||||
implementing "32 log-spaced bands, take the peak" will disagree on at least:
|
||||
|
||||
- `float` or `double` — the fixture has frames whose two strongest bands are
|
||||
within 1.3% of each other, so `float` is not sufficient;
|
||||
- periodic Hann (`/N`) or symmetric (`/(N-1)`);
|
||||
- whether a band's value is the **mean** or the **sum** of its magnitudes (sum
|
||||
favours wide high bands over narrow low ones);
|
||||
- which way an `argmax` breaks ties;
|
||||
- whether the frame count is `1 + (n - 4096)/1024` or something that wobbles
|
||||
with the resampler tail.
|
||||
|
||||
Every one of those is pinned at the top of
|
||||
[`AudioSignature.cs`](../Jellyfin.Plugin.JRay/Services/AudioSignature.cs) and
|
||||
matched in the C++ producer. A signature that differs in any parameter simply
|
||||
does not match, which defeats the entire point of having one.
|
||||
|
||||
### Two implementations, proven equal
|
||||
|
||||
The extraction pipeline (C++, `IR-004`) computes this for files it processes
|
||||
locally. The plugin (C#, `JR-042`) computes it for the files the pipeline never
|
||||
sees. Both must agree exactly.
|
||||
|
||||
That is a *checked* claim, not an aspiration. `fixtures/audio/` holds three files
|
||||
byte-identical to the extraction repo's copies:
|
||||
|
||||
| File | What it pins |
|
||||
|---|---|
|
||||
| `jray_audio_v1_tone.flac` | 120 s of tones stepping through all 32 bands, amplitudes walking a golden-ratio sequence so all four energy classes appear |
|
||||
| `jray_audio_v1_golden.json` | The expected signature, the band→FFT-bin table, and checksums of the decoded PCM |
|
||||
| `make_fixture.py` | Regenerates the media from plain arithmetic — no numpy, ports to any language in ~20 lines |
|
||||
|
||||
The binding check regenerates the fixture PCM from `make_fixture.py`'s arithmetic
|
||||
and verifies it against the recorded `s16le`/`f32le` checksums **before** making
|
||||
any DSP claim. So it runs on a CI host with no codec at all, and a decode
|
||||
divergence stays distinguishable from a DSP one. The two tests that drive real
|
||||
FFmpeg self-skip without a binary — a check that skips is not a check, so it is
|
||||
never the only cover for a claim.
|
||||
|
||||
---
|
||||
|
||||
## Matching, and the offset
|
||||
|
||||
Two signatures are compared by sliding one against the other:
|
||||
|
||||
```
|
||||
for offset in -600 .. +600 frames: # ±56 s
|
||||
score(offset) = fraction of overlapping frames whose peak band matches
|
||||
best = argmax score
|
||||
```
|
||||
|
||||
| Score | Meaning |
|
||||
|---|---|
|
||||
| `≥ 0.85` | Same cut. `audio` tier. The offset applies |
|
||||
| `0.60 – 0.85` | Possibly the same cut, degraded audio. `loose` tier, surfaced as a caveat |
|
||||
| `< 0.60` | Different content. No match |
|
||||
|
||||
Only the **peak band** is scored. The energy class is the coarser and less
|
||||
re-encoding-stable of the two fields, and the specification's rule names the peak
|
||||
bin alone.
|
||||
|
||||
Speed-differing releases (a PAL 4% speed-up) are not a constant offset and are
|
||||
correctly rejected by the score threshold rather than mis-aligned.
|
||||
|
||||
### The offset has two terms
|
||||
|
||||
This is the part that is easy to get wrong. Both windows are centred on **their
|
||||
own file's** midpoint, so when the runtimes differ the two windows do not start
|
||||
at the same point in the content:
|
||||
|
||||
```
|
||||
offset = (local_window_start - manifest_window_start) + slide × 1024/11025
|
||||
└────────── anchor difference ──────────┘ └──── recovered ────┘
|
||||
```
|
||||
|
||||
A release carrying 40 s of extra head material recovers **20 s from each term**.
|
||||
Using the slide alone would be wrong by half the runtime difference on every
|
||||
shifted release. The specification's pseudocode describes only the slide, because
|
||||
it is written from the server's position, where both signatures are being
|
||||
compared against one stored manifest.
|
||||
|
||||
### One parameter that is not from the specification
|
||||
|
||||
An alignment must overlap by at least **64 frames** (~6 s) before its score
|
||||
counts. Without a floor the extreme offsets compare a handful of frames, where a
|
||||
chance agreement scores 1.0 and beats the true alignment. It never binds on the
|
||||
real case: two full-length signatures still overlap by 688 frames at the widest
|
||||
offset. It is marked as a local addition in the code.
|
||||
|
||||
---
|
||||
|
||||
## What happens when a manifest arrives
|
||||
|
||||
`ManifestController.FetchItem` → `ManifestAligner.AlignAsync`, before anything is
|
||||
stored:
|
||||
|
||||
```
|
||||
1. Server returns a manifest, a tier, and an offset.
|
||||
2. Does the manifest carry cut.audio_signature? no -> use the server's offset
|
||||
Are signatures enabled in configuration? no -> use the server's offset
|
||||
3. Decode this file's centre window, compute its signature.
|
||||
4. Compare the two.
|
||||
match -> apply the LOCAL offset and tier
|
||||
no match -> apply the server's offset, record the disagreement
|
||||
not comparable -> apply the server's offset
|
||||
5. Apply the offset to every window, once, at store time.
|
||||
6. Write the truth file, and the alignment beside it.
|
||||
```
|
||||
|
||||
### Why the local answer wins
|
||||
|
||||
**The server has never seen your file.** Its offset is a claim about a runtime it
|
||||
was told — at best a runtime-difference inference. A local alignment compares the
|
||||
manifest's own signature against the media the windows will actually be drawn
|
||||
over, which is the authoritative comparison. It also needs no round trip, so no
|
||||
signature ever leaves the instance.
|
||||
|
||||
This is what the specification means by matching being "a consumer concern": the
|
||||
server never rewrites a manifest, so **one stored manifest serves every trim of
|
||||
the same cut**, and each client shifts it onto its own timebase.
|
||||
|
||||
### Degradation, never failure
|
||||
|
||||
A signature is an enhancement to cut matching. A missing one costs a tier and
|
||||
**must never be able to break a fetch**. Every one of these stores the manifest
|
||||
on the server's terms:
|
||||
|
||||
- signatures switched off in configuration (they are opt-in — the decode costs a
|
||||
second or two of I/O per item);
|
||||
- the item is under 120 s, so the window underflows and there is no signature to
|
||||
compute (`JR-044`);
|
||||
- the manifest carried no signature;
|
||||
- the manifest's signature is `v2:` from a future producer — **refused, not
|
||||
parsed** (`JR-045`), because scoring an unknown DSP chain as v1 would be a
|
||||
confident wrong answer where declining is a correct one;
|
||||
- no FFmpeg binary, no audio stream, or a decode error.
|
||||
|
||||
### "Un-comparable" and "does not match" are different
|
||||
|
||||
The distinction matters more than it looks. A 90-second extra is not content that
|
||||
disagrees with its manifest — it is content that could not be compared. Reporting
|
||||
the first as the second would show a user a scary warning about the wrong thing.
|
||||
|
||||
A genuine mismatch — both signatures present, both valid, both items long enough,
|
||||
and the best alignment still below 0.60 — is the strongest available hint that a
|
||||
manifest describes different content. It is **still not a failure**: the audio may
|
||||
legitimately differ, a different language track being the obvious case. So the
|
||||
manifest is stored on the server's terms and the disagreement is surfaced as a
|
||||
caveat, which outranks the tier's own caveat because it is the stronger
|
||||
statement.
|
||||
|
||||
---
|
||||
|
||||
## What gets stored
|
||||
|
||||
The offset is applied **once**, at store time, so the stored windows are always
|
||||
in your file's own timebase and no read path needs offset awareness (`JR-030`).
|
||||
That makes the offset unrecoverable afterwards — the windows look native — which
|
||||
is why the alignment is recorded beside the truth file:
|
||||
|
||||
```json
|
||||
"alignment": {
|
||||
"source": "Local",
|
||||
"tier": "Audio",
|
||||
"offset_sec": 40.0,
|
||||
"score": 0.97,
|
||||
"offset_frames": 215,
|
||||
"local_signature": "v1:AAAAA…",
|
||||
"server_offset_sec": 0.0,
|
||||
"server_tier": "Runtime"
|
||||
}
|
||||
```
|
||||
|
||||
`local_signature` is kept so a **later fetch aligns for free** — the decode is
|
||||
the expensive half and the reason signatures are opt-in. It never leaves the
|
||||
instance: provenance is stored beside the truth file, not inside it, and
|
||||
contribution strips provenance entirely (`JR-034`).
|
||||
|
||||
The truth file itself is untouched by any of this. Injecting fields would mean
|
||||
the bytes served back are not the bytes the producer wrote, which is the property
|
||||
`JR-004` turns on.
|
||||
|
||||
---
|
||||
|
||||
## Where the code is
|
||||
|
||||
| Concern | File |
|
||||
|---|---|
|
||||
| The DSP | [`Services/AudioSignature.cs`](../Jellyfin.Plugin.JRay/Services/AudioSignature.cs) |
|
||||
| The decode | [`Services/AudioSignatureService.cs`](../Jellyfin.Plugin.JRay/Services/AudioSignatureService.cs) |
|
||||
| Reading and matching | [`Services/AudioSignatureMatcher.cs`](../Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs) |
|
||||
| The fetch-path decision | [`Services/ManifestAligner.cs`](../Jellyfin.Plugin.JRay/Services/ManifestAligner.cs) |
|
||||
| What is recorded | [`Models/TruthAlignment.cs`](../Jellyfin.Plugin.JRay/Models/TruthAlignment.cs) |
|
||||
| Server-side validation | `JRay-public-server/src/validate.rs`, `validate_audio_signature` |
|
||||
| C++ producer | `scene-actor-extraction/src/audio_signature.{hpp,cpp}` |
|
||||
|
||||
Tests: `AudioSignatureTests` (UT-038 … UT-044), `AudioSignatureMatcherTests`
|
||||
(UT-045 … UT-052), `ManifestAlignerTests` (UT-053 … UT-057).
|
||||
|
||||
## Verifying the cross-repo claim by hand
|
||||
|
||||
```sh
|
||||
# C# side
|
||||
cd jRay && dotnet test Jellyfin.Plugin.JRay.Tests/Jellyfin.Plugin.JRay.Tests.csproj
|
||||
|
||||
# C++ side, against the same fixture
|
||||
cd scene-actor-extraction/build && python3 -c "
|
||||
import json, sae_audio
|
||||
g = json.load(open('../tests/fixtures/audio/jray_audio_v1_golden.json'))
|
||||
print(sae_audio.compute_signature('../tests/fixtures/audio/jray_audio_v1_tone.flac') == g['signature'])
|
||||
"
|
||||
|
||||
# and that the fixtures really are the same bytes
|
||||
md5sum jRay/Jellyfin.Plugin.JRay.Tests/fixtures/audio/jray_audio_v1_golden.json \
|
||||
scene-actor-extraction/tests/fixtures/audio/jray_audio_v1_golden.json
|
||||
```
|
||||
|
||||
## Not built yet
|
||||
|
||||
- **Matching against a server's catalogue.** `POST /manifests/search` and the
|
||||
`audio` tier server-side are `UR-009`, still in progress. The sequencing is
|
||||
deliberate — accumulate signatures first, enable matching once coverage is
|
||||
useful. Nothing here depends on it: alignment works from the signature the
|
||||
manifest already carries.
|
||||
- **Contribution.** Uploads do not yet attach a signature (`JR-034`).
|
||||
- **Re-using the stored signature.** It is written but not yet read back on a
|
||||
second fetch, so today every fetch decodes.
|
||||
+13
-1
@@ -72,6 +72,11 @@ Tag code with `// TRACES: JR-012 | SR-002`.
|
||||
| UT-050 | Identical signatures score 1.0 at offset 0 and reach the `audio` tier | JR-044 | **Passing** |
|
||||
| UT-051 | **A shifted release recovers its offset** rather than failing to match — the case the feature exists for | JR-044 | **Passing** |
|
||||
| UT-052 | Unrelated content yields **no match at all**, and runtime skew contributes its window-anchor term to the offset | JR-044 | **Passing** |
|
||||
| UT-053 | **A local alignment supersedes the server's offset**, and the server's claim is retained rather than overwritten | JR-047 | **Passing** |
|
||||
| UT-054 | The local signature is recorded, so a later fetch aligns without decoding the media again | JR-047 | **Passing** |
|
||||
| UT-055 | Every unavailable local path — off, no manifest signature, neither, short media, **and a `v2:` producer** — falls back to the server rather than refusing | JR-047 | **Passing** |
|
||||
| UT-056 | Two signatures that genuinely disagree are recorded as a mismatch and **still do not break the fetch** | JR-047 | **Passing** |
|
||||
| UT-057 | A mismatch **outranks the tier** in the caveat shown to the user — a `runtime` match would otherwise show nothing at all | JR-047 | **Passing** |
|
||||
|
||||
All execute and pass. UT-043 and UT-044 are the two that need an FFmpeg binary,
|
||||
which the plugin gets from Jellyfin at run time and a bare CI container may not
|
||||
@@ -216,18 +221,24 @@ preserved.
|
||||
| JR-040 | The config page states plainly that **each configured server multiplies the exposure** | **PR-005** | Medium | Planned |
|
||||
| JR-041 | The plugin never fetches, stores, or transmits gallery data — reference faces or embeddings. It has no gallery code path at all | **SR-005** | High | Done |
|
||||
|
||||
## Audio signature (JR-042 … JR-045)
|
||||
## Audio signature (JR-042 … JR-045, JR-047)
|
||||
|
||||
Mirror-image of extraction `IR-004`/`IR-005`/`IR-007`/`IR-008`. Both producers
|
||||
must agree **bit-for-bit**, so each obligation is stated on both sides rather
|
||||
than assumed to be inherited.
|
||||
|
||||
JR-042 … JR-045 are the signature itself, produced and read. **JR-047 is what
|
||||
uses it**: without a consumer on the fetch path the other four are a fingerprint
|
||||
nothing ever fingerprints. See [`audio-alignment.md`](audio-alignment.md) for the
|
||||
end-to-end mechanism.
|
||||
|
||||
| ID | Requirement | Traces to | Priority | Status |
|
||||
|---|---|---|---|---|
|
||||
| JR-042 | Compute the signature **exactly** per server spec §3, using the FFmpeg binary Jellyfin already ships via `IMediaEncoder.EncoderPath` — no new dependency | SR-003 | Medium | **Done** (UT-039…044) — `AudioSignature` is the DSP, `AudioSignatureService` the decode; FFmpeg is invoked as a child process for decode, downmix and resample, and nothing was added to the project's dependencies |
|
||||
| JR-043 | Golden-vector fixture **shared with the extraction repo**, proving the two implementations are bit-exact | SR-003 | High | **Done** (UT-038, UT-039) — `fixtures/audio/` holds the extraction repo's three files byte-identically; the C# signature equals the recorded vector exactly |
|
||||
| JR-044 | Media shorter than 120 s: emit no signature and apply no sync offset — identical rule in both producers | SR-003 | Low | **Done** (UT-045, UT-046, UT-050…052) — the producer half was already in `AudioSignatureService`; the consumer half needed a reader, so `AudioSignatureMatcher` implements the §3 slide and declines an offset outright below the window. The boundary is asserted on one file at 119.999 s and 120.000 s |
|
||||
| JR-045 | Emit and honour the signature's own `v1:` prefix, so a DSP change is detectable rather than silently non-matching | SR-003 | Low | **Done** (UT-047…049) — `AudioSignatureMatcher.TryParseFrames` refuses any prefix but `v1:`, and refuses malformed or structurally invalid payloads, so a future producer's `v2:` drops the item to the runtime tier instead of scoring as if it were understood |
|
||||
| JR-047 | **A fetched manifest is aligned against the local file before its windows are stored**, and the alignment is recorded beside the truth data | SR-003 | High | **Done** (UT-053…057) — `ManifestAligner` runs on the fetch path. A local alignment supersedes the server's offset, since the server has never seen this file; every unavailable path degrades to the server's offset rather than refusing, and a genuine signature disagreement is recorded and surfaced as a caveat without failing the fetch |
|
||||
|
||||
## Human-in-the-loop association (JR-046)
|
||||
|
||||
@@ -355,6 +366,7 @@ framework reference and leans on `RollForward` to reach the 10.0 runtime.
|
||||
| JR-043 | **T1** | Signature matches the shared golden vector **bit-for-bit** | The fixture PCM is regenerated from `make_fixture.py` and checked against the recorded decode checksums first, so the check binds on a host with no codec and a decode divergence is distinguishable from a DSP one |
|
||||
| JR-044 | T1 | Media < 120 s yields no signature and no offset | Exactly 120 s — the boundary both repos must agree on |
|
||||
| JR-045 | T1 | `v1:` emitted; an unknown prefix is refused, not parsed | `v2:` signature from a future producer |
|
||||
| JR-047 | **T1** | A fetched manifest is aligned locally before storage, and the alignment is recorded | **Every way the local path can be unavailable must degrade to the server's offset, never refuse** — signatures off, no manifest signature, media under the window, a `v2:` producer. Distinguish those from a genuine mismatch: a 90 s extra is not content that disagrees with its manifest, and telling a user it is would be worse than saying nothing |
|
||||
| JR-046 | T2 + **T4** | *Assertions deferred* — recording an association and persisting it is T2; the review UI itself is T4 | Cannot be written until the truth-file interface for unidentified presence is settled (system open question 2) and AR-021/AR-022 land |
|
||||
|
||||
Three are worth singling out. **JR-021** and **JR-041** are static checks because
|
||||
|
||||
+51
-10
@@ -3,7 +3,7 @@
|
||||
<!-- GENERATED FILE - do not edit by hand. -->
|
||||
<!-- Regenerate: scripts/traceability/traceability-gate.sh -->
|
||||
|
||||
**Generated:** 2026-07-31T14:19:46+00:00
|
||||
**Generated:** 2026-07-31T14:50:36+00:00
|
||||
|
||||
Denominators are read from [`requirements.md`](requirements.md) at run time, never hardcoded. Coverage counts a requirement only when it is tagged in source **and** has a verification tier this repo's CI host can execute (`T1, T2, static`).
|
||||
|
||||
@@ -11,13 +11,13 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
|
||||
|
||||
| Metric | Value |
|
||||
|---|---|
|
||||
| Source files scanned | 74 |
|
||||
| TRACES tags found | 45 |
|
||||
| Source files scanned | 78 |
|
||||
| TRACES tags found | 48 |
|
||||
| EXCEPTION tags found | 0 |
|
||||
| Requirements defined | 46 |
|
||||
| Requirements covered | 37 |
|
||||
| **Coverage** | **80.4%** (37/46) |
|
||||
| Coverage of CI-executable scope | 84.1% (37/44) |
|
||||
| Requirements defined | 47 |
|
||||
| Requirements covered | 38 |
|
||||
| **Coverage** | **80.9%** (38/47) |
|
||||
| Coverage of CI-executable scope | 84.4% (38/45) |
|
||||
| Tagged but unexecuted in CI | 1 |
|
||||
| Orphan tags | 0 |
|
||||
|
||||
@@ -25,9 +25,9 @@ Denominators are read from [`requirements.md`](requirements.md) at run time, nev
|
||||
|
||||
| Type | Covered | Tagged but unexecuted | Defined |
|
||||
|---|---|---|---|
|
||||
| JR | 37 | 1 | 46 |
|
||||
| JR | 38 | 1 | 47 |
|
||||
|
||||
- **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-002, UT-003, UT-004, UT-005, UT-007, UT-008, UT-009, UT-010, UT-011, UT-012, UT-013, UT-014, UT-015, UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023, UT-024, UT-025, UT-026, UT-027, UT-028, UT-029, UT-030, UT-031, UT-032, UT-033, UT-034, UT-035, UT-036, UT-037, UT-038, UT-039, UT-040, UT-041, UT-042, UT-043, UT-044, UT-045, UT-046, UT-047, UT-048, UT-049, UT-050, UT-051, UT-052
|
||||
- **UT** tags present (separate taxonomy, not counted in coverage): UT-001, UT-002, UT-003, UT-004, UT-005, UT-007, UT-008, UT-009, UT-010, UT-011, UT-012, UT-013, UT-014, UT-015, UT-016, UT-017, UT-018, UT-019, UT-020, UT-021, UT-022, UT-023, UT-024, UT-025, UT-026, UT-027, UT-028, UT-029, UT-030, UT-031, UT-032, UT-033, UT-034, UT-035, UT-036, UT-037, UT-038, UT-039, UT-040, UT-041, UT-042, UT-043, UT-044, UT-045, UT-046, UT-047, UT-048, UT-049, UT-050, UT-051, UT-052, UT-053, UT-054, UT-055, UT-056, UT-057
|
||||
- **PR** tags present (separate taxonomy, not counted in coverage): PR-001, PR-003, PR-004, PR-005, PR-006
|
||||
- **SR** tags present (separate taxonomy, not counted in coverage): SR-001, SR-002, SR-003, SR-004, SR-005
|
||||
|
||||
@@ -110,6 +110,7 @@ _None._
|
||||
| JR-044 | **Done** (UT-045, U… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs`, `Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs` | Media shorter than 120 s: emit no signature and apply no sync offset … |
|
||||
| JR-045 | **Done** (UT-047…04… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs`, `Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs` | Emit and honour the signature's own `v1:` prefix, so a DSP change is … |
|
||||
| JR-046 | **TBD** | T2, T4 | [system §4](../scripts/vendor/jray-proj… | untagged | - | Review UI for unidentified track clusters: show context crops, pick f… |
|
||||
| JR-047 | **Done** (UT-053…05… | T1 | SR-003 | covered | `Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs`, `Jellyfin.Plugin.JRay/Models/TruthAlignment.cs`, `Jellyfin.Plugin.JRay/Services/ManifestAligner.cs` | **A fetched manifest is aligned against the local file before its win… |
|
||||
|
||||
## Detailed mapping
|
||||
|
||||
@@ -384,6 +385,14 @@ _None._
|
||||
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs:24`](../Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs#L24) — `public static class AudioSignatureMatcher`
|
||||
- [`Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs:26`](../Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs#L26) — `public class AudioSignatureMatcherTests`
|
||||
|
||||
### JR-047
|
||||
|
||||
**Locations:** 3
|
||||
|
||||
- [`Jellyfin.Plugin.JRay/Models/TruthAlignment.cs:17`](../Jellyfin.Plugin.JRay/Models/TruthAlignment.cs#L17) — `public class TruthAlignment`
|
||||
- [`Jellyfin.Plugin.JRay/Services/ManifestAligner.cs:36`](../Jellyfin.Plugin.JRay/Services/ManifestAligner.cs#L36) — `public class ManifestAligner`
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
### PR-001
|
||||
|
||||
**Locations:** 3
|
||||
@@ -449,10 +458,11 @@ _None._
|
||||
|
||||
### SR-003
|
||||
|
||||
**Locations:** 11
|
||||
**Locations:** 13
|
||||
|
||||
- [`Jellyfin.Plugin.JRay/Controllers/TruthController.cs:28`](../Jellyfin.Plugin.JRay/Controllers/TruthController.cs#L28) — `public class TruthController : ControllerBase`
|
||||
- [`Jellyfin.Plugin.JRay/Models/Jmanifest.cs:21`](../Jellyfin.Plugin.JRay/Models/Jmanifest.cs#L21) — `public class Jmanifest`
|
||||
- [`Jellyfin.Plugin.JRay/Models/TruthAlignment.cs:17`](../Jellyfin.Plugin.JRay/Models/TruthAlignment.cs#L17) — `public class TruthAlignment`
|
||||
- [`Jellyfin.Plugin.JRay/Models/TruthCut.cs:15`](../Jellyfin.Plugin.JRay/Models/TruthCut.cs#L15) — `public class TruthCut`
|
||||
- [`Jellyfin.Plugin.JRay/Models/TruthExtraction.cs:21`](../Jellyfin.Plugin.JRay/Models/TruthExtraction.cs#L21) — `public class TruthExtraction`
|
||||
- [`Jellyfin.Plugin.JRay/Models/TruthFile.cs:26`](../Jellyfin.Plugin.JRay/Models/TruthFile.cs#L26) — `public class TruthFile`
|
||||
@@ -460,6 +470,7 @@ _None._
|
||||
- [`Jellyfin.Plugin.JRay/Services/AudioSignature.cs:49`](../Jellyfin.Plugin.JRay/Services/AudioSignature.cs#L49) — `public static class AudioSignature`
|
||||
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs:24`](../Jellyfin.Plugin.JRay/Services/AudioSignatureMatcher.cs#L24) — `public static class AudioSignatureMatcher`
|
||||
- [`Jellyfin.Plugin.JRay/Services/AudioSignatureService.cs:39`](../Jellyfin.Plugin.JRay/Services/AudioSignatureService.cs#L39) — `public class AudioSignatureService`
|
||||
- [`Jellyfin.Plugin.JRay/Services/ManifestAligner.cs:36`](../Jellyfin.Plugin.JRay/Services/ManifestAligner.cs#L36) — `public class ManifestAligner`
|
||||
- [`Jellyfin.Plugin.JRay/Services/ManifestConverter.cs:11`](../Jellyfin.Plugin.JRay/Services/ManifestConverter.cs#L11) — `public static class ManifestConverter`
|
||||
- [`Jellyfin.Plugin.JRay/Services/TruthSchema.cs:35`](../Jellyfin.Plugin.JRay/Services/TruthSchema.cs#L35) — `public static class TruthSchema`
|
||||
|
||||
@@ -783,3 +794,33 @@ _None._
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs:26`](../Jellyfin.Plugin.JRay.Tests/AudioSignatureMatcherTests.cs#L26) — `public class AudioSignatureMatcherTests`
|
||||
|
||||
### UT-053
|
||||
|
||||
**Locations:** 1
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
### UT-054
|
||||
|
||||
**Locations:** 1
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
### UT-055
|
||||
|
||||
**Locations:** 1
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
### UT-056
|
||||
|
||||
**Locations:** 1
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
### UT-057
|
||||
|
||||
**Locations:** 1
|
||||
|
||||
- [`Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs:28`](../Jellyfin.Plugin.JRay.Tests/ManifestAlignerTests.cs#L28) — `public class ManifestAlignerTests`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user