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:
+17
-31
@@ -20,7 +20,6 @@ pub struct ManifestRow {
|
||||
pub season: Option<i64>,
|
||||
pub episode: Option<i64>,
|
||||
pub runtime_sec: f64,
|
||||
pub video_hash: Option<String>,
|
||||
pub audio_signature: Option<Vec<u8>>,
|
||||
pub sample_fps: Option<f64>,
|
||||
pub extinction_sec: Option<f64>,
|
||||
@@ -31,7 +30,7 @@ pub struct ManifestRow {
|
||||
pub content_id: Option<String>,
|
||||
}
|
||||
|
||||
const MANIFEST_COLUMNS: &str = "id, title_id, season, episode, runtime_sec, video_hash, \
|
||||
const MANIFEST_COLUMNS: &str = "id, title_id, season, episode, runtime_sec, \
|
||||
audio_signature, sample_fps, extinction_sec, pipeline_version, gallery_scope, \
|
||||
status, cast_match_ratio, \
|
||||
content_id";
|
||||
@@ -43,15 +42,14 @@ fn map_manifest(row: &rusqlite::Row<'_>) -> rusqlite::Result<ManifestRow> {
|
||||
season: row.get(2)?,
|
||||
episode: row.get(3)?,
|
||||
runtime_sec: row.get(4)?,
|
||||
video_hash: row.get(5)?,
|
||||
audio_signature: row.get(6)?,
|
||||
sample_fps: row.get(7)?,
|
||||
extinction_sec: row.get(8)?,
|
||||
pipeline_version: row.get(9)?,
|
||||
gallery_scope: row.get(10)?,
|
||||
status: row.get(11)?,
|
||||
cast_match_ratio: row.get(12)?,
|
||||
content_id: row.get(13)?,
|
||||
audio_signature: row.get(5)?,
|
||||
sample_fps: row.get(6)?,
|
||||
extinction_sec: row.get(7)?,
|
||||
pipeline_version: row.get(8)?,
|
||||
gallery_scope: row.get(9)?,
|
||||
status: row.get(10)?,
|
||||
cast_match_ratio: row.get(11)?,
|
||||
content_id: row.get(12)?,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -283,24 +281,21 @@ pub fn duplicate_from_contributor(
|
||||
season: Option<i64>,
|
||||
episode: Option<i64>,
|
||||
runtime_sec: f64,
|
||||
video_hash: Option<&str>,
|
||||
contributor_id: &str,
|
||||
) -> anyhow::Result<Option<String>> {
|
||||
let mut stmt = tx.prepare_cached(
|
||||
"SELECT id FROM manifests
|
||||
WHERE title_id = ?1 AND contributor_id = ?6
|
||||
WHERE title_id = ?1 AND contributor_id = ?5
|
||||
AND status <> 'rejected'
|
||||
AND ( (?2 IS NULL AND season IS NULL) OR season = ?2 )
|
||||
AND ( (?3 IS NULL AND episode IS NULL) OR episode = ?3 )
|
||||
AND ABS(runtime_sec - ?4) < 0.001
|
||||
AND ( (?5 IS NULL AND video_hash IS NULL) OR video_hash = ?5 )
|
||||
LIMIT 1",
|
||||
)?;
|
||||
Ok(stmt
|
||||
.query_row(
|
||||
params![title_id, season, episode, runtime_sec, video_hash, contributor_id],
|
||||
|r| r.get::<_, String>(0),
|
||||
)
|
||||
.query_row(params![title_id, season, episode, runtime_sec, contributor_id], |r| {
|
||||
r.get::<_, String>(0)
|
||||
})
|
||||
.optional()?)
|
||||
}
|
||||
|
||||
@@ -319,7 +314,6 @@ pub struct NewManifest<'a> {
|
||||
pub season: Option<i64>,
|
||||
pub episode: Option<i64>,
|
||||
pub runtime_sec: f64,
|
||||
pub video_hash: Option<&'a str>,
|
||||
pub audio_signature: Option<&'a [u8]>,
|
||||
pub audio_sig_coarse: Option<&'a [u8]>,
|
||||
pub sample_fps: Option<f64>,
|
||||
@@ -338,18 +332,17 @@ pub struct NewManifest<'a> {
|
||||
pub fn insert_manifest(tx: &Transaction<'_>, m: &NewManifest<'_>) -> anyhow::Result<()> {
|
||||
tx.execute(
|
||||
"INSERT INTO manifests
|
||||
(id, title_id, season, episode, runtime_sec, video_hash,
|
||||
(id, title_id, season, episode, runtime_sec,
|
||||
audio_signature, audio_sig_coarse, sample_fps, extinction_sec, pipeline_version,
|
||||
gallery_scope, contributor_id, status, content_id, origin, ingested_from,
|
||||
created_at)
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17,?18)",
|
||||
VALUES (?1,?2,?3,?4,?5,?6,?7,?8,?9,?10,?11,?12,?13,?14,?15,?16,?17)",
|
||||
params![
|
||||
m.id,
|
||||
m.title_id,
|
||||
m.season,
|
||||
m.episode,
|
||||
m.runtime_sec,
|
||||
m.video_hash,
|
||||
m.audio_signature,
|
||||
m.audio_sig_coarse,
|
||||
m.sample_fps,
|
||||
@@ -840,7 +833,6 @@ mod tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 6420.5,
|
||||
video_hash: Some("opensubtitles:8e245d9679d31e12"),
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: Some(5.0),
|
||||
@@ -903,7 +895,6 @@ mod tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 100.0,
|
||||
video_hash: None,
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: None,
|
||||
@@ -949,7 +940,6 @@ mod tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 6420.5,
|
||||
video_hash: None,
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: None,
|
||||
@@ -964,11 +954,10 @@ mod tests {
|
||||
created_at: NOW,
|
||||
},
|
||||
)?;
|
||||
let same = duplicate_from_contributor(tx, &title_id, None, None, 6420.5, None, &a)?;
|
||||
let same = duplicate_from_contributor(tx, &title_id, None, None, 6420.5, &a)?;
|
||||
// §7: multiple manifests for the same cut from *different*
|
||||
// contributors are allowed, and ranked.
|
||||
let other =
|
||||
duplicate_from_contributor(tx, &title_id, None, None, 6420.5, None, &b)?;
|
||||
let other = duplicate_from_contributor(tx, &title_id, None, None, 6420.5, &b)?;
|
||||
Ok((same.is_some(), other.is_some()))
|
||||
})
|
||||
.await
|
||||
@@ -1002,7 +991,6 @@ mod tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 100.0,
|
||||
video_hash: None,
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: None,
|
||||
@@ -1108,7 +1096,6 @@ mod tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 100.0,
|
||||
video_hash: None,
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: None,
|
||||
@@ -1402,7 +1389,6 @@ mod federation_tests {
|
||||
season: None,
|
||||
episode: None,
|
||||
runtime_sec: 100.0,
|
||||
video_hash: None,
|
||||
audio_signature: None,
|
||||
audio_sig_coarse: None,
|
||||
sample_fps: None,
|
||||
|
||||
+2
-2
@@ -41,7 +41,8 @@ CREATE TABLE IF NOT EXISTS manifests (
|
||||
season INTEGER,
|
||||
episode INTEGER,
|
||||
runtime_sec REAL NOT NULL,
|
||||
video_hash TEXT,
|
||||
-- No video_hash: withdrawn (§3). It fingerprinted an individual file rather
|
||||
-- than a cut, which is the one thing this schema deliberately cannot record.
|
||||
audio_signature BLOB, -- §3, ~1290 bytes
|
||||
audio_sig_coarse BLOB, -- candidate-generation index key
|
||||
sample_fps REAL,
|
||||
@@ -160,7 +161,6 @@ CREATE INDEX IF NOT EXISTS idx_federation_log_content ON federation_log(content_
|
||||
CREATE INDEX IF NOT EXISTS idx_titles_tmdb ON titles(tmdb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_titles_imdb ON titles(imdb_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_manifests_title_runtime ON manifests(title_id, runtime_sec);
|
||||
CREATE INDEX IF NOT EXISTS idx_manifests_video_hash ON manifests(video_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_manifests_episode ON manifests(title_id, season, episode);
|
||||
CREATE INDEX IF NOT EXISTS idx_scenes_manifest_person ON scenes(manifest_id, tmdb_person_id);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user