fix(readarr): attach editions when toggling book monitoring

Toggling book monitoring failed with an HTTP 500, "Value cannot be null.
(Parameter 'source')", every time, in both the CLI and the TUI.

GET /book/{id} omits the editions field entirely, but PUT /book/{id}
requires it, so round-tripping Readarr's own response back as an update
was always rejected. Editions are served separately from
GET /edition?bookId=, so fetch them and attach them to the body before
the PUT.
This commit is contained in:
Dark-Alex-17 committed 2026-09-11 11:27:27 -06:00
1 parent fbf711f428
commit 9dbd985506
2 files changed
+135 -4

No files matched your search

@@ -275,10 +275,34 @@ impl Network<'_, '_> {
})
.await?;
info!("Fetching book editions for book with ID: {book_id}");
let editions_event = ReadarrEvent::GetBookEditions(book_id);
let request_props = self
.request_props_from(
editions_event,
RequestMethod::Get,
None::<()>,
None,
Some(format!("bookId={book_id}")),
)
.await;
let mut editions_response = String::new();
self
.handle_request::<(), Value>(request_props, |editions_body, _| {
editions_response = editions_body.to_string()
})
.await?;
info!("Constructing toggle book monitoring body");
match serde_json::from_str::<Value>(&response) {
Ok(mut detailed_book_body) => {
match (
serde_json::from_str::<Value>(&response),
serde_json::from_str::<Value>(&editions_response),
) {
(Ok(mut detailed_book_body), Ok(editions)) => {
let monitored = detailed_book_body
.get("monitored")
.unwrap()
@@ -286,6 +310,10 @@ impl Network<'_, '_> {
.unwrap();
*detailed_book_body.get_mut("monitored").unwrap() = json!(!monitored);
detailed_book_body
.as_object_mut()
.unwrap()
.insert("editions".to_owned(), editions);
debug!("Toggle book monitoring body: {detailed_book_body:?}");
@@ -303,8 +331,8 @@ impl Network<'_, '_> {
.handle_request::<Value, ()>(request_props, |_, _| ())
.await
}
Err(_) => {
warn!("Request for detailed book body was interrupted");
_ => {
warn!("Request for detailed book body or book editions was interrupted");
Ok(())
}
}
@@ -1020,15 +1020,53 @@ mod tests {
#[case] initial_monitored: bool,
#[case] expected_monitored: bool,
) {
let editions_json = json!([
{
"id": 2,
"bookId": 1,
"foreignEditionId": "test-foreign-edition-id-2",
"monitored": false,
"isEbook": false,
"title": "Second Edition",
"language": "eng",
"overview": "the second edition",
"format": "Hardcover",
"publisher": "Second Publisher",
"pageCount": 128,
"releaseDate": "2023-02-01T00:00:00Z",
"isbn13": "9780000000002",
"asin": "B000000002",
"ratings": { "votes": 15, "value": 8.4, "popularity": 1.2 }
},
serde_json::from_str::<Value>(EDITION_JSON).unwrap()
]);
let mut book_json: Value = serde_json::from_str(BOOK_JSON).unwrap();
*book_json.get_mut("monitored").unwrap() = json!(initial_monitored);
let mut expected_body = book_json.clone();
*expected_body.get_mut("monitored").unwrap() = json!(expected_monitored);
expected_body
.as_object_mut()
.unwrap()
.insert("editions".to_owned(), editions_json.clone());
let (async_details_server, app, mut server) = MockServarrApi::get()
.returns(book_json)
.path("/1")
.build_for(ReadarrEvent::GetBookDetails(1))
.await;
let async_editions_server = server
.mock(
"GET",
format!(
"/api/v1{}?bookId=1",
ReadarrEvent::GetBookEditions(1).resource()
)
.as_str(),
)
.with_status(200)
.match_header("X-Api-Key", "test1234")
.with_body(editions_json.to_string())
.create_async()
.await;
let async_toggle_server = server
.mock(
"PUT",
@@ -1051,8 +1089,10 @@ mod tests {
.await;
async_details_server.assert_async().await;
async_editions_server.assert_async().await;
async_toggle_server.assert_async().await;
assert_ok!(result);
assert_none!(app.lock().await.data.readarr_data.book_details_modal);
}
#[tokio::test]
@@ -1063,6 +1103,20 @@ mod tests {
.path("/1")
.build_for(ReadarrEvent::GetBookDetails(1))
.await;
let async_editions_server = server
.mock(
"GET",
format!(
"/api/v1{}?bookId=1",
ReadarrEvent::GetBookEditions(1).resource()
)
.as_str(),
)
.with_status(200)
.match_header("X-Api-Key", "test1234")
.expect(0)
.create_async()
.await;
let async_toggle_server = server
.mock(
"PUT",
@@ -1085,6 +1139,55 @@ mod tests {
.await;
async_details_server.assert_async().await;
async_editions_server.assert_async().await;
async_toggle_server.assert_async().await;
assert_err!(result);
}
#[tokio::test]
async fn test_handle_toggle_book_monitoring_event_editions_failure() {
let (async_details_server, app, mut server) = MockServarrApi::get()
.returns(serde_json::from_str(BOOK_JSON).unwrap())
.path("/1")
.build_for(ReadarrEvent::GetBookDetails(1))
.await;
let async_editions_server = server
.mock(
"GET",
format!(
"/api/v1{}?bookId=1",
ReadarrEvent::GetBookEditions(1).resource()
)
.as_str(),
)
.with_status(404)
.match_header("X-Api-Key", "test1234")
.with_body(json!([serde_json::from_str::<Value>(EDITION_JSON).unwrap()]).to_string())
.create_async()
.await;
let async_toggle_server = server
.mock(
"PUT",
format!(
"/api/v1{}/1",
ReadarrEvent::ToggleBookMonitoring(1).resource()
)
.as_str(),
)
.with_status(202)
.match_header("X-Api-Key", "test1234")
.expect(0)
.create_async()
.await;
app.lock().await.server_tabs.set_index(3);
let mut network = test_network(&app);
let result = network
.handle_readarr_event(ReadarrEvent::ToggleBookMonitoring(1))
.await;
async_details_server.assert_async().await;
async_editions_server.assert_async().await;
async_toggle_server.assert_async().await;
assert_err!(result);
}