feat(api): add movie CRUD API #56
Reference in New Issue
Block a user
Delete Branch "issue/29-movie-api"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Add movie CRUD, intent/override updates, manual search and release-grab actions.
Return classified movie releases and derived attention queues.
Generate OpenAPI coverage and SQLx offline query metadata.
Issue
Closes #29## Test plan
just ci\n- Generate the OpenAPI TypeScript schema and compile it with TypeScript 5.9.3Reviewed
f61a2b7.pool()is reachable on every DB endpoint but documented only onlistandattention;updatecan also return 409 through the unique/FK mapping. The OpenAPI responses undersell the error surface.@@ -0,0 +293,4 @@Path(id): Path<i64>,) -> Result<(StatusCode, Json<Accepted>), ApiError> {load_movie(&state, id).await?;state.send_movie_command(MovieCommand::Search { movie_id: id });This queues a targeted search for blocked movies. DESIGN.md §6.3:
blocked"stops targeted search for a title entirely". Nothing rejects it here, and no command consumer exists yet to filter it — either 409 onblockedor the reconcile side must check.@@ -0,0 +340,4 @@responses((status = 200, body = AttentionQueues), (status = 503, body = ErrorBody)))]pub async fn attention(State(state): State<AppState>) -> Result<Json<AttentionQueues>, ApiError> {let no_pt_source = sqlx::query_as!(Movie, r#"SELECT id AS "id!: i64", tmdb_id AS "tmdb_id!: i64", title AS "title!: String", year, original_language, root_id AS "root_id!: i64", wanted AS "wanted!: bool", overrides AS "overrides!: serde_json::Value", state AS "state!: String", blocked AS "blocked!: bool", search_attempts AS "search_attempts!: i64", last_searched_at FROM movies WHERE id IN (SELECT m.id FROM movies m JOIN roots root ON root.id = m.root_id WHERE root.audience = 'kids' AND m.wanted = 1 AND m.state = 'missing' AND m.search_attempts > 0 AND NOT EXISTS (SELECT 1 FROM movie_releases mr JOIN releases r ON r.id = mr.release_id WHERE mr.movie_id = m.id AND r.verdict IN ('eligible', 'waived'))) ORDER BY title"#)no_pt_sourcedoes not exclude blocked movies (m.blocked = 0). A title the operator parked reappears in the attention queue it was meant to be dismissed from.@@ -73,12 +86,38 @@ impl AppState {/// If the TLS backend cannot be initialised.pub fn new(upstreams: Upstreams) -> Result<Self, reqwest::Error> {let http = reqwest::Client::builder().timeout(PROBE_TIMEOUT).build()?;let (movie_commands, pending_movie_commands) = broadcast::channel(64);broadcast::channel(64)with a single stored receiver: once the daemon consumer lags 64 commands behind, the oldest are dropped silently andnext_movie_commandreturnsLagged— a grab that was answered 202 vanishes. With exactly one consumer,mpscfits better (lossless, backpressure); otherwise handleLaggedexplicitly.Addressed in
4357fcf: every movie endpoint now documents its reachable database/error responses, and the attention queues have semantic tests covering search attempts, eligible releases, and two distinct failed grabs.Reviewed
4357fcf. The response documentation and the attention-queue test match what was asked; the test covers both queue transitions correctly.Still open from the previous review: the broadcast command channel dropping lagged commands (state.rs), and
blockedbeing ignored byPOST /searchand theno_pt_sourcequeue (movies.rs).Addressed in
f40b758: movie actions now use a backpressured MPSC channel instead of lossy broadcast delivery. Blocked movies return 409 from manual search and are excluded fromno_pt_source; the queue test covers both cases.Reviewed
f40b758. All three open findings are addressed: mpsc replaces the lossy broadcast channel with send failures surfaced as 503,POST /searchreturns 409 for blocked movies, andno_pt_sourceexcludes them — each with test coverage.One note, not blocking:
send().awaiton a full queue parks the request until the consumer drains — and the daemon has no consumer until the reconcile loop lands, so the 65th queued command hangs its HTTP request.try_sendmapped to 503 would keep the API responsive either way.No other findings.
Applied the non-blocking note in
b2e4e15: action submission now usestry_send, maps a full queue to 503 immediately, and has a saturation test.Reviewed
b2e4e15.try_sendwith 503 on a full queue, with a test. No findings.