Withdraw the file-hash tier; document the legal posture
Removes `cut.video_hash` and the `exact` match tier on legal grounds. The OpenSubtitles hash was the strongest technical signal available — it identifies a specific file, so it cannot produce a false positive — and that is exactly the problem. Every tier must be a claim about a *cut*, never about a copy. A TMDB id discloses "some copy of this film", which is what a library catalogue discloses. A file hash discloses "this exact release": it made a read endpoint into a release-level oracle, and made an instance's database 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 asked to hold. The audio signature is the replacement: derived from content, it identifies the cut rather than the copy, so two encodes of the same edit agree. The field is deleted rather than kept as a vestigial null, on the same reasoning §2 applied to `anneal_sec` — a key naming a signal the format no longer has is actively misleading — so an upload carrying one is now an unknown-field 400, with a test asserting it. **Every content_id changes**, including for manifests that never carried a hash, because the canonical `cut` object lost a key. The golden vector is regenerated and re-verified against an independent Python implementation; the plugin and extraction repos must adopt the new value or federation deduplication silently breaks. Free now, pre-release; not free later. Adds docs/legal-posture.md, the operator-facing half of what §5a asks for: what an instance holds exhaustively, what it structurally cannot do, and how that sits against the intermediary-liability regimes that plausibly apply. 208 tests. Coverage 25/32. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: UR-011 | SR-004, PR-005
This commit is contained in:
+1
-6
@@ -128,12 +128,7 @@ async fn lookup_one(state: &AppState, params: &LookupParams) -> ApiResult<Exists
|
||||
|
||||
let cuts: Vec<(String, StoredCut)> = candidates
|
||||
.iter()
|
||||
.map(|m| {
|
||||
(
|
||||
m.id.clone(),
|
||||
StoredCut { runtime_sec: m.runtime_sec, video_hash: m.video_hash.clone() },
|
||||
)
|
||||
})
|
||||
.map(|m| (m.id.clone(), StoredCut { runtime_sec: m.runtime_sec }))
|
||||
.collect();
|
||||
|
||||
let Some((id, m)) = matching::best_match(&client_cut, &cuts) else {
|
||||
|
||||
+1
-3
@@ -99,8 +99,7 @@ async fn fetch_best(
|
||||
let cuts: Vec<(ManifestRow, StoredCut)> = candidates
|
||||
.into_iter()
|
||||
.map(|m| {
|
||||
let cut =
|
||||
StoredCut { runtime_sec: m.runtime_sec, video_hash: m.video_hash.clone() };
|
||||
let cut = StoredCut { runtime_sec: m.runtime_sec };
|
||||
(m, cut)
|
||||
})
|
||||
.collect();
|
||||
@@ -347,7 +346,6 @@ pub fn reconstruct(
|
||||
cut: Cut {
|
||||
runtime_sec: row.runtime_sec,
|
||||
container_duration_sec: None,
|
||||
video_hash: row.video_hash.clone(),
|
||||
audio_signature: None,
|
||||
},
|
||||
extraction: has_extraction.then_some(extraction),
|
||||
|
||||
@@ -21,7 +21,6 @@ pub struct LookupParams {
|
||||
pub season: Option<i64>,
|
||||
pub episode: Option<i64>,
|
||||
pub runtime_sec: Option<f64>,
|
||||
pub video_hash: Option<String>,
|
||||
}
|
||||
|
||||
impl LookupParams {
|
||||
@@ -30,7 +29,6 @@ impl LookupParams {
|
||||
// A non-finite or non-positive runtime is not a usable signal; treat
|
||||
// it as absent rather than letting it drive a match.
|
||||
runtime_sec: self.runtime_sec.filter(|r| r.is_finite() && *r > 0.0),
|
||||
video_hash: self.video_hash.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user