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
174 lines
7.7 KiB
SQL
174 lines
7.7 KiB
SQL
-- §7 Storage. Fully relational, no JSON blobs on the write path: the database
|
|
-- can only represent what the schema models, so there is physically nowhere for
|
|
-- an unexpected field or a smuggled string to live (§5a Threat 1).
|
|
--
|
|
-- Portable SQL — runs unchanged on Postgres. Avoid SQLite-specific forms
|
|
-- (`INSERT OR REPLACE`); use `INSERT ... ON CONFLICT` (§8 deployment notes).
|
|
|
|
CREATE TABLE IF NOT EXISTS contributors (
|
|
id TEXT PRIMARY KEY,
|
|
token_hash TEXT NOT NULL UNIQUE,
|
|
created_at TEXT NOT NULL,
|
|
revoked_at TEXT,
|
|
accepted_count INTEGER NOT NULL DEFAULT 0,
|
|
rejected_count INTEGER NOT NULL DEFAULT 0,
|
|
flagged_count INTEGER NOT NULL DEFAULT 0
|
|
);
|
|
|
|
-- Server-side, TMDB-derived. `name` never comes from an upload (§5a).
|
|
CREATE TABLE IF NOT EXISTS people (
|
|
tmdb_person_id INTEGER PRIMARY KEY,
|
|
name TEXT NOT NULL,
|
|
adult INTEGER NOT NULL DEFAULT 0,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS titles (
|
|
id TEXT PRIMARY KEY,
|
|
kind TEXT NOT NULL, -- movie | series
|
|
tmdb_id TEXT,
|
|
imdb_id TEXT,
|
|
name TEXT,
|
|
year INTEGER,
|
|
adult INTEGER NOT NULL DEFAULT 0,
|
|
certification TEXT,
|
|
updated_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS manifests (
|
|
id TEXT PRIMARY KEY,
|
|
title_id TEXT NOT NULL REFERENCES titles(id),
|
|
season INTEGER,
|
|
episode INTEGER,
|
|
runtime_sec REAL NOT NULL,
|
|
-- 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,
|
|
extinction_sec REAL, -- successor to the withdrawn anneal_sec
|
|
gallery_scope TEXT, -- limited | global; ranking signal (§2, §7)
|
|
pipeline_version TEXT,
|
|
contributor_id TEXT REFERENCES contributors(id),
|
|
status TEXT NOT NULL, -- pending | listed | flagged | rejected
|
|
reject_reason TEXT,
|
|
cast_match_ratio REAL,
|
|
content_id TEXT UNIQUE, -- §9a, sha256 over canonical form
|
|
origin TEXT, -- server_id of first acceptance
|
|
ingested_from TEXT, -- peer id, NULL if uploaded directly
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS manifest_actors (
|
|
manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE,
|
|
tmdb_person_id INTEGER NOT NULL,
|
|
PRIMARY KEY (manifest_id, tmdb_person_id)
|
|
);
|
|
|
|
-- Integer centiseconds, not floats — the same quantisation used for
|
|
-- `content_id`, so stored values and hashed values cannot diverge (§7, §9a).
|
|
CREATE TABLE IF NOT EXISTS scenes (
|
|
manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE,
|
|
tmdb_person_id INTEGER NOT NULL,
|
|
start_cs INTEGER NOT NULL,
|
|
end_cs INTEGER NOT NULL,
|
|
-- Per-window provenance (SR-003, extraction AR-017). Deliberately NOT part
|
|
-- of `content_id`: belief is a producer-side estimate that may differ
|
|
-- between pipeline versions for identical timings, so hashing it would give
|
|
-- two servers different ids for the same content (§9a).
|
|
belief REAL,
|
|
route TEXT -- live | deferred | pooled
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS reports (
|
|
id TEXT PRIMARY KEY,
|
|
manifest_id TEXT NOT NULL REFERENCES manifests(id) ON DELETE CASCADE,
|
|
reason TEXT NOT NULL,
|
|
note TEXT,
|
|
created_at TEXT NOT NULL,
|
|
source_ip_hash TEXT
|
|
);
|
|
|
|
-- The sole JSON column, and it holds TMDB's responses, not users' (§7).
|
|
CREATE TABLE IF NOT EXISTS tmdb_cache (
|
|
tmdb_id TEXT NOT NULL,
|
|
kind TEXT NOT NULL,
|
|
credits TEXT NOT NULL,
|
|
fetched_at TEXT NOT NULL,
|
|
PRIMARY KEY (tmdb_id, kind)
|
|
);
|
|
|
|
-- Background queue as a table rather than an external broker, so pending work
|
|
-- survives a restart (§7, §8).
|
|
CREATE TABLE IF NOT EXISTS jobs (
|
|
id TEXT PRIMARY KEY,
|
|
kind TEXT NOT NULL, -- cast_check | federation_pull
|
|
payload TEXT NOT NULL,
|
|
run_after TEXT NOT NULL,
|
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
last_error TEXT,
|
|
leased_at TEXT
|
|
);
|
|
|
|
-- §9a federation. Peering is trust-by-configuration: a row exists only because
|
|
-- an operator typed a URL. Nothing a remote server says can create one.
|
|
CREATE TABLE IF NOT EXISTS peers (
|
|
id TEXT PRIMARY KEY,
|
|
url TEXT NOT NULL UNIQUE,
|
|
name TEXT,
|
|
enabled INTEGER NOT NULL DEFAULT 0,
|
|
pull_interval_sec INTEGER NOT NULL DEFAULT 3600,
|
|
-- Auto-delist on a peer's legal retraction. Off by default: a retraction
|
|
-- that delists automatically is a remote delete primitive over your
|
|
-- catalogue, so it is opt-in per peer between operators who know each other.
|
|
trust_abuse_retractions INTEGER NOT NULL DEFAULT 0,
|
|
-- Publishing a peering is opt-in on BOTH sides (§9a): peering with someone
|
|
-- must not advertise their existence against their wishes.
|
|
advertise INTEGER NOT NULL DEFAULT 0,
|
|
max_ingest_per_hour INTEGER NOT NULL DEFAULT 500,
|
|
peered_since TEXT,
|
|
last_cursor TEXT,
|
|
last_pull_at TEXT,
|
|
last_error TEXT
|
|
);
|
|
|
|
-- The change feed. Append-only, so a peer can resume from an opaque cursor and
|
|
-- the feed is idempotent. A separate table rather than deriving the feed from
|
|
-- `manifests` because a *retraction* is an event with no surviving row.
|
|
CREATE TABLE IF NOT EXISTS federation_log (
|
|
-- A genuinely monotonic sequence, NOT a ULID.
|
|
--
|
|
-- ULIDs are only monotonic *between* milliseconds: two generated in the same
|
|
-- millisecond carry independent random components, so they can sort in the
|
|
-- opposite order to which they were written. A peer resuming from `seq >
|
|
-- cursor` would then silently skip an entry — replication losing manifests
|
|
-- with no error anywhere, which is the worst shape a bug can take here.
|
|
--
|
|
-- AUTOINCREMENT (rather than plain rowid) additionally guarantees the value
|
|
-- never decreases even after deletions. Portability note (§8): Postgres
|
|
-- spells this `BIGSERIAL PRIMARY KEY`; it is the one place a monotonic
|
|
-- sequence has no fully portable form, and it is worth the exception.
|
|
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
content_id TEXT NOT NULL,
|
|
op TEXT NOT NULL, -- add | retract
|
|
reason TEXT, -- retract only; 'abuse' is the one that may auto-delist
|
|
origin TEXT NOT NULL, -- server_id that first accepted it
|
|
created_at TEXT NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_federation_log_content ON federation_log(content_id);
|
|
|
|
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_episode ON manifests(title_id, season, episode);
|
|
CREATE INDEX IF NOT EXISTS idx_scenes_manifest_person ON scenes(manifest_id, tmdb_person_id);
|
|
|
|
-- All read queries filter `status IN ('listed','flagged')`, so a partial index
|
|
-- on that predicate keeps the hot path small (§7).
|
|
CREATE INDEX IF NOT EXISTS idx_manifests_served
|
|
ON manifests(title_id, season, episode)
|
|
WHERE status IN ('listed', 'flagged');
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_jobs_ready ON jobs(run_after);
|