Implements §9a. The replication surface is four reads and no writes: a change feed, fetch by content_id, a batch have, and a human-facing peer directory — plus a capabilities endpoint carrying the accepted envelope versions, which lets a client discover a schema mismatch in one request instead of a 400 per manifest across a library sweep. Pull, never push: a pulling server chooses what it ingests and when. Push would let any peer inject work into the validation queue — the same abuse surface as anonymous upload, at higher volume. Nothing inherits a peer's judgement. A pulled manifest runs the full §6 stage 1 and 2 validation and this server's own cast check, and the fetched body must hash to the content_id that was asked for — the check that stops an intermediary or a misbehaving peer substituting content under a trusted id. A peer's retraction flags for review rather than delisting, because auto-delisting would hand every peer a remote delete primitive; only the opt-in per-peer abuse channel delists, because a takedown propagating at the speed of manual review is the wrong failure mode for that one case. A test caught a real bug in the first cut: the feed cursor was a ULID, and ULIDs are only monotonic *between* milliseconds — two generated in the same millisecond carry independent random components and can sort opposite to write order. A peer resuming from `seq > cursor` would then silently skip an entry: replication losing manifests with no error anywhere. The cursor is now an AUTOINCREMENT integer, and the test asserts strict monotonicity rather than merely sortedness. Peer administration is deliberately not an API. §9a requires that a peering exist only because an operator typed a URL, so nothing a remote server returns can establish or widen one; there_is_no_endpoint_that_creates_a_peering asserts that absence rather than trusting it. 212 tests. Coverage 25/32 (78%). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> TRACES: UR-008 | PR-006
37 lines
1022 B
Rust
37 lines
1022 B
Rust
//! HTTP surface (§4). Base path `/api/v1`, JSON throughout.
|
|
|
|
pub mod exists;
|
|
pub mod federation;
|
|
pub mod fetch;
|
|
pub mod json;
|
|
pub mod report;
|
|
pub mod upload;
|
|
|
|
use serde::Deserialize;
|
|
|
|
use crate::matching::ClientCut;
|
|
|
|
/// Identity + cut query parameters, shared by the read endpoints (§4).
|
|
#[derive(Debug, Clone, Default, Deserialize)]
|
|
pub struct LookupParams {
|
|
pub tmdb_id: Option<String>,
|
|
pub imdb_id: Option<String>,
|
|
pub series_tmdb_id: Option<String>,
|
|
pub series_imdb_id: Option<String>,
|
|
pub season: Option<i64>,
|
|
pub episode: Option<i64>,
|
|
pub runtime_sec: Option<f64>,
|
|
pub video_hash: Option<String>,
|
|
}
|
|
|
|
impl LookupParams {
|
|
pub fn client_cut(&self) -> ClientCut {
|
|
ClientCut {
|
|
// 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(),
|
|
}
|
|
}
|
|
}
|