Initial implementation: core vertical slice
Implements the core of SPEC.md — the manifest exchange, less audio-tier matching (§3) and federation (§9a), both of which the spec sequences as later work. - §2 Jmanifest format and series bundles - §3 cut matching: exact / runtime / loose tiers - §4 API, less POST /manifests/search - §5 rate limiting; §5a trust model, anonymous bearer tokens - §6 upload validation, all four stages - §7 relational storage, no JSON blob on the write path - §8 Rust + Axum + SQLite, single serialized writer, in-process job queue - §9a content addressing, computed on upload Reconciled against the system spec: - anneal_sec removed, withdrawn upstream by AR-012/AR-013. Presence follows track extent, so a track survives its own gaps and there is nothing to anneal. Its successor extinction_sec and the new gallery_scope are accepted and stored; scope enters the §7 ranking. A manifest still carrying anneal_sec is a hard 400, not silently ignored — it came from a pipeline whose window semantics differ from what this server assumes. - Audio signature: media under 120 s now emits no signature at all, matching scene-actor-extraction IR-007. The earlier §3 draft allowed a shortened window under 150 s, which was the weaker rule — a caller-varying length is the property SR-004 forbids. - UR IDs regularised to UR-nnn; docs/requirements.md registers 32 requirements, each tracing to an SR-nnn or PR-nnn. 189 tests: unit, end-to-end through the real router, and an injection suite covering SQL, JSON, header and Unicode payloads. Writing that suite found two real gaps, both fixed here: compatibility homoglyphs passed the §5a character class, and a one-frame audio signature was accepted on a feature-length item. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,119 @@
|
||||
-- §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,
|
||||
video_hash TEXT,
|
||||
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
|
||||
);
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
-- 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);
|
||||
Reference in New Issue
Block a user