feat(api): add movie CRUD API #56
Merged
naps62-yolo
merged 5 commits from 2026-08-22 20:40:31 +01:00
issue/29-movie-api into main
Labels
Clear labels
area/api
area/ci
area/compat
area/core
area/daemon
area/db
area/dl
area/indexer
area/infra
area/meta
area/parse
area/probe
area/subs
area/web
difficulty/easy
difficulty/hard
difficulty/moderate
difficulty/trivial
phase/1-skeleton
phase/2-logic
phase/3-sourcing
phase/4-movies
phase/5-ui
phase/6-tv
phase/7-people
phase/8-compat
phase/9-subtitles
type/bug
type/chore
type/feature
type/test
arr-api
CI and test harness
arr-compat
arr-core
arr-daemon
arr-db
arr-dl
arr-indexer
workspace and tooling
arr-meta
arr-parse
arr-probe
arr-subs crate: providers, extraction, translation, sync
web/
bounded, obvious approach, few files
subtle correctness or cross-cutting
multiple files, judgement, an interface to design
one file, mechanical, no design decisions
workspace, CI, config, database, empty API and SPA
parsing and policy engine, pure, no network
TMDB and Prowlarr, read-only
movies end to end
search, buckets, library views, queues
seasons, episodes, tracking, derived status
owner tags and notifications
Jellyseerr shim
build order: subtitles, DESIGN.md §15
No labels
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: yolo/arr#56
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
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.