Skip to content

Address ABS importer failures#1802

Open
IgnisDa wants to merge 6 commits into
mainfrom
fix-abs
Open

Address ABS importer failures#1802
IgnisDa wants to merge 6 commits into
mainfrom
fix-abs

Conversation

@IgnisDa

@IgnisDa IgnisDa commented Jul 23, 2026

Copy link
Copy Markdown
Owner

Refactor get_item_details to intercept each failure mode separately:

  • Network/transport errors (connect refused, timeout, DNS)
  • Non-2xx HTTP status codes (with response body)
  • JSON parse failures

Populate lot (MediaLot) in all ImportFailedItem error paths so the media type is always visible in failure reports. Include the Audiobookshelf item.id and episode.id in error messages for cross-referencing. Fix commit_metadata failures to use ImportFailStep::DatabaseCommit instead of ItemDetailsFromSource.

Summary by CodeRabbit

  • Bug Fixes
    • Improved Audiobookshelf podcast importing by precomputing finished episodes and skipping them during import.
    • Reduced repeated Audiobookshelf item/episode detail lookups for faster, smoother processing.
    • Tightened error handling with clearer messages, including item identifiers, HTTP status, and response content where applicable.
    • Improved robustness when required identifiers are missing by explicitly setting the appropriate import failure fields.
    • Added retrying and more contextual diagnostics for Audiobookshelf item detail fetching.

IgnisDa added 2 commits July 23, 2026 22:33
…ypes and richer context

Refactor get_item_details to intercept each failure mode separately:
- Network/transport errors (connect refused, timeout, DNS)
- Non-2xx HTTP status codes (with response body)
- JSON parse failures

Populate lot (MediaLot) in all ImportFailedItem error paths so the media
type is always visible in failure reports. Include the Audiobookshelf
item.id and episode.id in error messages for cross-referencing. Fix
commit_metadata failures to use ImportFailStep::DatabaseCommit instead
of ItemDetailsFromSource.
@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

Audiobookshelf importing now loads completion progress, filters podcast episodes to finished entries, fetches item details once per podcast, and reports lookup, validation, HTTP, parsing, and commit failures with item identifiers and processing metadata.

Changes

Audiobookshelf podcast completion import

Layer / File(s) Summary
Progress contract and loading
crates/models/external/src/lib.rs, crates/services/importer/audiobookshelf/src/lib.rs
Adds public progress response models, builds an authenticated client, and loads finished (library_item_id, episode_id) pairs from Audiobookshelf’s /me endpoint into the importer context.
Podcast episode filtering
crates/services/importer/audiobookshelf/src/lib.rs
Fetches podcast details once, skips episodes without IDs, filters episodes by completion membership, and commits metadata only for matching episodes while processing items serially.
Import error context
crates/services/importer/audiobookshelf/src/lib.rs
Adds explicit failure lot and step values, item identifiers, formatted underlying errors, and detailed HTTP status, URL, and response-body diagnostics.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Importer
  participant AudiobookshelfAPI
  participant Database
  Importer->>AudiobookshelfAPI: request /me progress
  AudiobookshelfAPI-->>Importer: completed media progress
  Importer->>AudiobookshelfAPI: request podcast item details
  AudiobookshelfAPI-->>Importer: podcast episodes
  Importer->>Importer: filter completed episodes
  Importer->>Database: commit matching metadata
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and clearly relates to the importer failure-handling changes in this PR.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix-abs

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 121-135: Update the InputTransformation failure messages in the
book import branches to include both the existing reason and the Audiobookshelf
record identifier item.id. Also update the episode database-commit failure near
the episode_id handling to include episode_id in its error message, preserving
the existing failure steps and identifiers.
- Around line 130-134: Reorder the fields in the affected ImportFailedItem
initializers to follow ascending source-line length, placing the shorter lot and
step fields before the longer error expressions. Apply this consistently to
every initializer in the importer, including the instances around the No ISBN
found path and the additional referenced locations, without changing values or
behavior.
- Around line 258-272: Update the response-body read in the Audiobookshelf
request flow to replace unwrap_or_default with error propagation using
with_context(...)? before the status check. Preserve the existing status
handling and JSON parsing behavior while reporting failures to read
response.text() with request URL context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 88fb2da5-7e8b-4bcc-8eb0-bc3ab5b68bee

📥 Commits

Reviewing files that changed from the base of the PR and between dcca39c and beb4d12.

📒 Files selected for processing (1)
  • crates/services/importer/audiobookshelf/src/lib.rs

Comment on lines 121 to 135
return Err(ImportFailedItem {
identifier: title,
lot: Some(MediaLot::Book),
step: ImportFailStep::InputTransformation,
error: Some("No Google Books ID found".to_string()),
..Default::default()
});
}
},
_ => {
return Err(ImportFailedItem {
identifier: title,
lot: Some(MediaLot::Book),
error: Some("No ISBN found".to_string()),
step: ImportFailStep::InputTransformation,
..Default::default()
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Include all source identifiers in failure messages.

The book failures at Lines 125 and 133 should include item.id. The episode database-commit failure at Line 186 should also include episode_id; otherwise failures cannot identify the exact Audiobookshelf record.

Also applies to: 184-189

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 121 - 135,
Update the InputTransformation failure messages in the book import branches to
include both the existing reason and the Audiobookshelf record identifier
item.id. Also update the episode database-commit failure near the episode_id
handling to include episode_id in its error message, preserving the existing
failure steps and identifiers.

Comment on lines 130 to 134
return Err(ImportFailedItem {
identifier: title,
lot: Some(MediaLot::Book),
error: Some("No ISBN found".to_string()),
step: ImportFailStep::InputTransformation,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Reorder struct fields by ascending line length.

The changed ImportFailedItem initializers should consistently sort their fields by source-line length; notably, move shorter lot/step fields before the longer error expressions. As per coding guidelines, Rust struct fields must be ordered by ascending line length.

Also applies to: 143-148, 160-169, 184-189, 213-220

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 130 - 134,
Reorder the fields in the affected ImportFailedItem initializers to follow
ascending source-line length, placing the shorter lot and step fields before the
longer error expressions. Apply this consistently to every initializer in the
importer, including the instances around the No ISBN found path and the
additional referenced locations, without changing values or behavior.

Source: Coding guidelines

Comment on lines +258 to +272
let request_url = format!("{url}/items/{id}");
let response = client
.get(&request_url)
.query(&query)
.send()
.await?
.json::<audiobookshelf_models::Item>()
.await?;
.await
.with_context(|| format!("Network error requesting audiobookshelf API at {request_url}"))?;
let status = response.status();
let body = response.text().await.unwrap_or_default();
if !status.is_success() {
anyhow::bail!("Audiobookshelf returned HTTP {status} (url: {request_url}): {body}");
}
let item = serde_json::from_str::<audiobookshelf_models::Item>(&body).with_context(|| {
format!("Failed to parse JSON from Audiobookshelf response for {request_url}")
})?;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant file and surrounding implementation.
if [ -f "crates/services/importer/audiobookshelf/src/lib.rs" ]; then
  echo "== file exists =="
  wc -l crates/services/importer/audiobookshelf/src/lib.rs
  echo "== relevant lines =="
  sed -n '220,290p' crates/services/importer/audiobookshelf/src/lib.rs | nl -ba -v220
else
  echo "missing target file"
  fd -a 'lib\.rs$' crates/services/importer 2>/dev/null | sed -n '1,50p'
fi

echo "== imports and error types =="
rg -n "reqwest|response\.text|with_context|audiobookshelf|struct .*Error|type .*Result|use anyhow" crates/services/importer/audiobookshelf -S

Repository: IgnisDa/ryot

Length of output: 284


🏁 Script executed:

#!/bin/bash
set -euo pipefail

file="crates/services/importer/audiobookshelf/src/lib.rs"
echo "== relevant lines =="
sed -n '220,290p' "$file" | awk '{ printf "%6d  %s\n", 220+NR-1, $0 }'

echo "== imports and error types / response usage =="
rg -n "reqwest|response\.text|with_context|audiobookshelf|struct .*Error|type .*Result|use anyhow" crates/services/importer/audiobookshelf -S || true

echo "== dependency info =="
for f in Cargo.toml crates/services/importer/importer/Cargo.toml crates/services/importer/audiobookshelf/Cargo.toml; do
  if [ -f "$f" ]; then
    echo "-- $f --"
    rg -n "reqwest|anyhow|serde_json|tokio" "$f" || true
  fi
done

echo "== reqwest text result type availability in docs if installed =="
cargo metadata --format-version 1 --no-deps >/tmp/cargo-metadata.json 2>/tmp/cargo-metadata.err || true
if [ -f /tmp/cargo-metadata.json ]; then
  python3 - <<'PY' /tmp/cargo-metadata.json
import json, sys
meta=json.load(open(sys.argv[1]))
packages=[p for p in meta.get("packages",[]) if p.get("name")=="reqwest"]
for p in packages:
    print(p.get("name"), p.get("version"), p.get("source"))
PY
fi

Repository: IgnisDa/ryot

Length of output: 4613


🌐 Web query:

reqwest Response text documentation returns Result String Error

💡 Result:

In the reqwest crate, the text method on the Response struct returns a Result<String, reqwest::Error> [1][2]. When calling this method, the response body is decoded into a String. It includes several behaviors to ensure robust decoding: - Encoding Determination: It prioritizes the charset parameter of the Content-Type header; if that is not present, it defaults to UTF-8 [1][3]. - Character Handling: Malformed sequences are replaced with the char::REPLACEMENT_CHARACTER [1][4]. - BOM Handling: Byte Order Marks (BOM) are detected and stripped from the returned String [1][2]. If the optional charset feature is disabled, the method only attempts to decode the response as UTF-8, regardless of the Content-Type header [1][3].

Citations:


Propagate response-body read failures.

response.text().await.unwrap_or_default() converts read failures into an empty body. Failures are then misreported as JSON parsing errors for successful responses, or lost for HTTP error responses. Preserve the read error with with_context(...)? before checking the status.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 258 - 272,
Update the response-body read in the Audiobookshelf request flow to replace
unwrap_or_default with error propagation using with_context(...)? before the
status check. Preserve the existing status handling and JSON parsing behavior
while reporting failures to read response.text() with request URL context.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
crates/services/importer/audiobookshelf/src/lib.rs (1)

159-164: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

ImportFailedItem struct-literal fields not ordered by ascending line length. All three sites still violate the field-ordering guideline flagged in a prior review round.

  • crates/services/importer/audiobookshelf/src/lib.rs#L159-L164: reorder to identifier, lot, step, error.
  • crates/services/importer/audiobookshelf/src/lib.rs#L192-L197: reorder to identifier, lot, step, error.
  • crates/services/importer/audiobookshelf/src/lib.rs#L221-L229: reorder to lot, identifier, step, error (lot: None, is shorter than identifier: title,).

As per coding guidelines, "When initializing structs (Rust) or object literals (TypeScript), order fields by ascending line length - shorter lines first, longer lines last."

🛠️ Proposed fixes
             .map_err(|e| ImportFailedItem {
                 identifier: title.clone(),
-                error: Some(format!("item_id={}: {e:#}", item.id)),
                 lot: Some(MediaLot::Podcast),
                 step: ImportFailStep::ItemDetailsFromSource,
+                error: Some(format!("item_id={}: {e:#}", item.id)),
             })?;
...
             .map_err(|e| ImportFailedItem {
                 identifier: title.clone(),
-                error: Some(format!("item_id={}: {e:#}", item.id)),
                 lot: Some(MediaLot::Podcast),
                 step: ImportFailStep::DatabaseCommit,
+                error: Some(format!("item_id={}: {e:#}", item.id)),
             })?;
...
             return Err(ImportFailedItem {
-                identifier: title,
-                step: ImportFailStep::InputTransformation,
                 lot: None,
+                identifier: title,
+                step: ImportFailStep::InputTransformation,
                 error: Some(format!(
                     "item_id={}: No ASIN, ISBN or iTunes ID found",
                     item.id
                 )),
             });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 159 - 164,
Reorder the ImportFailedItem struct literals in
crates/services/importer/audiobookshelf/src/lib.rs at lines 159-164 and 192-197
to identifier, lot, step, error, and at lines 221-229 to lot, identifier, step,
error, preserving all field values and behavior.

Source: Coding guidelines

♻️ Duplicate comments (2)
crates/services/importer/audiobookshelf/src/lib.rs (2)

260-276: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Response-body read failures are still swallowed before the status check.

response.text().await.unwrap_or_default() converts a body-read failure into an empty string. On success status, that empty body is then handed to serde_json::from_str, misreporting what is actually a transport/read failure as a JSON parse error. On a non-success status, the real read error is silently lost from the bail! message. Propagate it with .with_context(...)? before the status check, as previously flagged.

🛠️ Proposed fix
-    let status = response.status();
-    let body = response.text().await.unwrap_or_default();
+    let status = response.status();
+    let body = response
+        .text()
+        .await
+        .with_context(|| format!("Failed to read response body from {request_url}"))?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 260 - 276,
Update the response-body read in the item-fetching function to propagate
failures instead of using unwrap_or_default. Add contextual error handling to
response.text().await and apply ? before the status check, preserving the
existing HTTP-status and JSON parsing behavior for successfully read bodies.

137-151: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Failure messages still omit source-record identifiers needed to trace records. Both sites build ImportFailedItem.error without the full identifying context of the Audiobookshelf record that failed, a gap flagged in a previous review round and still unaddressed.

  • crates/services/importer/audiobookshelf/src/lib.rs#L137-L151: add item.id to the "No Google Books ID found" and "No ISBN found" error strings.
  • crates/services/importer/audiobookshelf/src/lib.rs#L192-L197: add episode_id (in addition to the already-present item.id) to the commit_metadata failure error string.
🛠️ Proposed fix
-                        error: Some("No Google Books ID found".to_string()),
+                        error: Some(format!("item_id={}: No Google Books ID found", item.id)),
...
-                        error: Some("No ISBN found".to_string()),
+                        error: Some(format!("item_id={}: No ISBN found", item.id)),
...
-                            error: Some(format!("item_id={}: {e:#}", item.id)),
+                            error: Some(format!(
+                                "item_id={}, episode_id={}: {e:#}",
+                                item.id, episode_id
+                            )),
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 137 - 151,
The Audiobookshelf import failure messages lack complete source-record context.
In crates/services/importer/audiobookshelf/src/lib.rs:137-151, update the “No
Google Books ID found” and “No ISBN found” errors to include item.id; in
crates/services/importer/audiobookshelf/src/lib.rs:192-197, update the
commit_metadata failure error to include episode_id alongside the existing
item.id.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 52-64: The /me response parsing currently panics via unwrap
instead of propagating errors. In the import flow around the MeResponse
deserialization, replace unwrap with error propagation using ? and add
contextual information with with_context, preserving the existing Result-based
handling.

---

Outside diff comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 159-164: Reorder the ImportFailedItem struct literals in
crates/services/importer/audiobookshelf/src/lib.rs at lines 159-164 and 192-197
to identifier, lot, step, error, and at lines 221-229 to lot, identifier, step,
error, preserving all field values and behavior.

---

Duplicate comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 260-276: Update the response-body read in the item-fetching
function to propagate failures instead of using unwrap_or_default. Add
contextual error handling to response.text().await and apply ? before the status
check, preserving the existing HTTP-status and JSON parsing behavior for
successfully read bodies.
- Around line 137-151: The Audiobookshelf import failure messages lack complete
source-record context. In
crates/services/importer/audiobookshelf/src/lib.rs:137-151, update the “No
Google Books ID found” and “No ISBN found” errors to include item.id; in
crates/services/importer/audiobookshelf/src/lib.rs:192-197, update the
commit_metadata failure error to include episode_id alongside the existing
item.id.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: b1b842ea-dfdd-40e8-963a-df520bb2edf8

📥 Commits

Reviewing files that changed from the base of the PR and between beb4d12 and 71c2f16.

📒 Files selected for processing (2)
  • crates/models/external/src/lib.rs
  • crates/services/importer/audiobookshelf/src/lib.rs

Comment on lines +52 to +64
let me = client
.get(format!("{url}/me"))
.send()
.await?
.json::<audiobookshelf_models::MeResponse>()
.await
.unwrap();
let finished_podcast_episodes = me
.media_progress
.into_iter()
.filter(|progress| progress.is_finished)
.filter_map(|progress| Some((progress.library_item_id?, progress.episode_id?)))
.collect::<HashSet<_>>();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

/me response parsing panics on failure, unlike the rest of this PR.

.json::<MeResponse>().await.unwrap() panics the entire import job if the /me endpoint returns malformed JSON or an unexpected shape — a much larger blast radius than the per-item failures this PR otherwise carefully categorizes (network vs. status vs. parse errors) in get_item_details. Since import() already returns Result<ImportResult>, propagate this with ? and with_context instead.

🛠️ Proposed fix
-    let me = client
-        .get(format!("{url}/me"))
-        .send()
-        .await?
-        .json::<audiobookshelf_models::MeResponse>()
-        .await
-        .unwrap();
+    let me = client
+        .get(format!("{url}/me"))
+        .send()
+        .await
+        .with_context(|| "Network error requesting Audiobookshelf /me endpoint")?
+        .json::<audiobookshelf_models::MeResponse>()
+        .await
+        .with_context(|| "Failed to parse JSON from Audiobookshelf /me response")?;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let me = client
.get(format!("{url}/me"))
.send()
.await?
.json::<audiobookshelf_models::MeResponse>()
.await
.unwrap();
let finished_podcast_episodes = me
.media_progress
.into_iter()
.filter(|progress| progress.is_finished)
.filter_map(|progress| Some((progress.library_item_id?, progress.episode_id?)))
.collect::<HashSet<_>>();
let me = client
.get(format!("{url}/me"))
.send()
.await
.with_context(|| "Network error requesting Audiobookshelf /me endpoint")?
.json::<audiobookshelf_models::MeResponse>()
.await
.with_context(|| "Failed to parse JSON from Audiobookshelf /me response")?;
let finished_podcast_episodes = me
.media_progress
.into_iter()
.filter(|progress| progress.is_finished)
.filter_map(|progress| Some((progress.library_item_id?, progress.episode_id?)))
.collect::<HashSet<_>>();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 52 - 64, The
/me response parsing currently panics via unwrap instead of propagating errors.
In the import flow around the MeResponse deserialization, replace unwrap with
error propagation using ? and add contextual information with with_context,
preserving the existing Result-based handling.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

♻️ Duplicate comments (1)
crates/services/importer/audiobookshelf/src/lib.rs (1)

143-148: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Missing lot field — likely a compile-breaking regression.

This ImportFailedItem literal only sets identifier, error, and step; the required lot: Option<MediaLot> field is absent and there's no ..Default::default() spread. ImportFailedItem's fields (identifier, step, lot, error) are all required in a struct literal unless the struct derives Default and a .. update syntax is used — neither is shown here. This will fail with E0063: missing field lot unless ImportFailedItem derives Default. This also contradicts the AI summary, which states both the Google Books ID and ISBN branches now explicitly set lot: Some(MediaLot::Book). It also still omits item.id in the error message, unresolved from a prior review round.

🐛 Proposed fix
                 _ => {
                     return Err(ImportFailedItem {
                         identifier: title,
-                        error: Some("No ISBN found".to_string()),
-                        step: ImportFailStep::InputTransformation,
+                        lot: Some(MediaLot::Book),
+                        step: ImportFailStep::InputTransformation,
+                        error: Some(format!("item_id={}: No ISBN found", item.id)),
                     });
                 }
#!/bin/bash
# Confirm whether ImportFailedItem derives Default (would allow the omitted field to compile).
rg -n -B5 'struct ImportFailedItem' crates/models/importer/src/lib.rs
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 143 - 148,
Update the ImportFailedItem construction in the ISBN-missing branch to include
the required lot field as Some(MediaLot::Book), matching the Google Books ID
branch. Also include item.id in the “No ISBN found” error message, while
preserving the existing identifier and InputTransformation step.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 254-285: Update get_item_details and fetch_item_details error
handling so retries occur only for network failures or HTTP 5xx responses;
immediately return parse errors and other non-retryable statuses such as 4xx
responses. Preserve the existing retry count, backoff, and successful item
return behavior for retryable failures.
- Line 96: Replace the global buffer_unordered(1) limit in the importer stream
with scoped synchronization, such as bounded concurrency or a per-podcast lock.
Preserve concurrency between independent library items while ensuring each
podcast’s episode imports and commits remain coordinated during item-details
retries.
- Around line 309-322: Update build_client to handle HeaderValue::from_str
failure without unwrapping: propagate the invalid-token error to the importer so
it returns one failed import item instead of panicking. Adjust the function’s
return flow and its caller as needed while preserving normal client creation and
existing client-build behavior.

---

Duplicate comments:
In `@crates/services/importer/audiobookshelf/src/lib.rs`:
- Around line 143-148: Update the ImportFailedItem construction in the
ISBN-missing branch to include the required lot field as Some(MediaLot::Book),
matching the Google Books ID branch. Also include item.id in the “No ISBN found”
error message, while preserving the existing identifier and InputTransformation
step.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: cde7441e-ebc3-4f04-8d4e-382f278792db

📥 Commits

Reviewing files that changed from the base of the PR and between 71c2f16 and e2984e3.

📒 Files selected for processing (1)
  • crates/services/importer/audiobookshelf/src/lib.rs

let results: Vec<_> = stream::iter(finished_items.results.into_iter().enumerate())
.map(|(idx, item)| process_item(idx, item, len, &client, &url, &services))
.buffer_unordered(5)
.buffer_unordered(1)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

🧩 Analysis chain

🌐 Web query:

Is there a documented reason to fully serialize reqwest requests instead of using bounded concurrency with buffer_unordered?

💡 Result:

There is no documented requirement or technical advantage to fully serializing reqwest requests as a default practice. In fact, reqwest is designed for asynchronous operation, and using concurrency (such as with buffer_unordered) is the standard approach to maximize throughput and minimize latency by hiding network wait times [1][2][3]. However, there are specific, documented scenarios where serializing requests or limiting concurrency is necessary or preferred over unbound parallelism: 1. Server-Side Rate Limiting and Resource Constraints: Sending too many concurrent requests can trigger rate limits (429 Too Many Requests) or overwhelm the destination server, leading to dropped connections or timeouts [4]. 2. Client-Side Resource Exhaustion: Without bound, you risk exhausting local system resources, such as file descriptors or socket limits [4]. Connection pooling in reqwest is highly efficient, but unbounded concurrency can still cause connection churn, especially when interacting with many different hosts [5][3]. 3. Avoiding Thundering Herd Problems: In some complex architectures, spawning too many futures at once can create contention issues, where tasks wake up repeatedly but fail to progress, wasting CPU cycles [6]. 4. Order Sensitivity: If your application logic depends on the requests completing in a strict sequential order (e.g., streaming a process where step B requires the output of step A), buffer_unordered will break this sequence. In such cases, standard sequential iteration or buffer_ordered is required [7]. 5. Complex Data Dependencies: If requests are not independent—for example, if they require shared mutable state or perform operations that are not thread-safe (though reqwest::Client handles connection pooling internally and safely across threads)—serialization may be the simplest way to avoid complex synchronization overhead [1][5]. To balance performance with these constraints, it is common to use buffer_unordered(n) with a carefully chosen n (bounded concurrency) or a tokio::sync::Semaphore to manually control the number of active requests without resorting to full serialization [4][8]. The reqwest::Client should generally be created once and shared (cloned) across tasks to benefit from connection pooling, regardless of whether you choose to run requests concurrently or sequentially [5][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Locate and inspect the relevant importer implementation and retry flow.
fd -a 'lib.rs' . | sed 's#^\./##' | rg 'importer/audiobookshelf/src/lib.rs|audiobookshelf/src/lib.rs' || true

echo '--- outline ---'
ast-grep outline crates/services/importer/audiobookshelf/src/lib.rs --view compact || true

echo '--- relevant lines ---'
cat -n crates/services/importer/audiobookshelf/src/lib.rs | sed -n '1,180p'

echo '--- search get_item_details and retry/backoff symbols ---'
rg -n "get_item_details|buffer_unordered|retry|backoff|try|Result|async|match" crates/services/importer/audiobookshelf/src/lib.rs

Repository: IgnisDa/ryot

Length of output: 9866


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- read slice around item processing continuation and get_item_details ---'
cat -n crates/services/importer/audiobookshelf/src/lib.rs | sed -n '150,320p'

echo '--- get_item_details usages and concurrent limits in audiobookshelf importer ---'
rg -n "get_item_details|buffer_unordered|buffer_max|Semaphore|semaphore|mutex|lock|finished_podcast|podcast" crates/services/importer/audiobookshelf/src/lib.rs crates -g '*.rs' | head -200

echo '--- determine whether get_item_details loop is exponential backoff and how many sleeps ---'
python3 - <<'PY'
from pathlib import Path
s = Path('crates/services/importer/audiobookshelf/src/lib.rs').read_text().splitlines()
for i, line in enumerate(s, 1):
    if i < 250 or i > 310:
        continue
    print(f"{i:04d}: {line}")
PY

Repository: IgnisDa/ryot

Length of output: 32688


Avoid serializing the entire importer with buffer_unordered(1).

Each podcast imports per-episode commits, while item-details fetching can retry forever with exponential sleeps (2s then 4s before the final attempt). Limit the risk with scoped synchronization instead of making every library item fetch and commit purely sequential; a bounded stream or per-podcast lock would preserve concurrency without exposing the same stale-during-retry failure window across the whole import pipeline.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` at line 96, Replace the
global buffer_unordered(1) limit in the importer stream with scoped
synchronization, such as bounded concurrency or a per-podcast lock. Preserve
concurrency between independent library items while ensuring each podcast’s
episode imports and commits remain coordinated during item-details retries.

Comment on lines +254 to +285
const MAX_ITEM_DETAILS_ATTEMPTS: u32 = 3;

async fn get_item_details(
client: &Client,
url: &str,
id: &str,
episode: Option<String>,
) -> Result<audiobookshelf_models::Item> {
let mut query = serde_json::json!({ "expanded": "1", "include": "progress" });
if let Some(episode) = episode {
query["episode"] = serde_json::json!(episode);
let query = serde_json::json!({ "expanded": "1" });
let request_url = format!("{url}/items/{id}");
let mut last_error = None;
for attempt in 0..MAX_ITEM_DETAILS_ATTEMPTS {
match fetch_item_details(client, &request_url, &query).await {
Ok(item) => return Ok(item),
Err(error) => {
last_error = Some(error);
if attempt + 1 < MAX_ITEM_DETAILS_ATTEMPTS {
let sleep_time = u64::pow(2, attempt + 1);
ryot_log!(
debug,
"Attempt {}/{} failed for {}, retrying in {}s",
attempt + 1,
MAX_ITEM_DETAILS_ATTEMPTS,
request_url,
sleep_time
);
sleep_for_n_seconds(sleep_time).await;
}
}
}
}
let item = client
.get(format!("{url}/items/{id}"))
.query(&query)
Err(last_error.unwrap())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚀 Performance & Scalability | 🟠 Major | 🏗️ Heavy lift

Retry loop retries non-transient failures (parse errors, 4xx) the same as transient ones.

fetch_item_details errors from serde_json::from_str (malformed/unexpected JSON shape) and from non-success HTTP statuses (e.g. a deleted item returning 404) are indistinguishable from network errors here, so every permanent failure now burns the full 3-attempt exponential backoff (~2s + 4s ≈ 6s) before surfacing. Combined with buffer_unordered(1) (Line 96), this can meaningfully slow down imports for libraries containing any permanently-unreachable items. Consider only retrying on network errors or 5xx statuses, and failing fast on 4xx/parse errors.

[reliability_and_resilience]

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 254 - 285,
Update get_item_details and fetch_item_details error handling so retries occur
only for network failures or HTTP 5xx responses; immediately return parse errors
and other non-retryable statuses such as 4xx responses. Preserve the existing
retry count, backoff, and successful item return behavior for retryable
failures.

Comment on lines +309 to 322
fn build_client(bearer_token: &str, danger_accept_invalid_certs: bool) -> Client {
let mut headers = HeaderMap::new();
headers.insert(USER_AGENT, HeaderValue::from_static(USER_AGENT_STR));
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {bearer_token}")).unwrap(),
);
ClientBuilder::new()
.default_headers(headers)
.timeout(Duration::from_secs(60))
.danger_accept_invalid_certs(danger_accept_invalid_certs)
.build()
.unwrap()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does reqwest HeaderValue::from_str reject leading/trailing whitespace or control characters in API tokens?

💡 Result:

Yes, the HeaderValue::from_str method in the http crate (which reqwest uses) rejects strings containing leading or trailing whitespace [1][2]. When you attempt to create a HeaderValue from a string that starts or ends with whitespace (such as a space or tab), the operation will return an error [1][2]. This design choice follows the HTTP specification (RFC 7230/9110), which dictates that header field values should not include leading or trailing whitespace, as such characters are meant to be excluded by parsers [1][2]. In addition to rejecting leading and trailing whitespace, HeaderValue::from_str validates that the input contains only visible ASCII characters (bytes 32–126, where 32 is a space) [3][4][5]. Although the horizontal tab (byte 9) is technically allowed in header values by the underlying logic, the specific restriction against leading/trailing whitespace prevents its use at the beginning or end of the string [1][5]. If you are dealing with API tokens that may contain accidental whitespace, you should trim the string (e.g., using.trim in Rust) before passing it to HeaderValue::from_str [2].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg 'crates/services/importer/audiobookshelf/src/lib\.rs|Cargo\.toml|Cargo.lock' || true

echo "== inspect relevant lines =="
cat -n crates/services/importer/audiobookshelf/src/lib.rs | sed -n '260,350p'

echo "== imports and surrounding config/token usage =="
sed -n '1,120p' crates/services/importer/audiobookshelf/src/lib.rs
rg -n "api_key|bearer_token|build_client|ImportResult|ImportFailedItem" crates/services/importer/audiobookshelf/src/lib.rs

echo "== reqwest/http dependency versions =="
rg -n 'reqwest|http \||http =|http ' Cargo.toml Cargo.lock

Repository: IgnisDa/ryot

Length of output: 15699


🌐 Web query:

http crate 1.4.0 HeaderValue::from_str leading whitespace leading/hard tabs v1 value parsing

💡 Result:

In the http crate, the HeaderValue::from_str method (and related constructors like from_bytes) strictly prohibits leading or trailing whitespace, as well as control characters (including tabs and other non-visible ASCII characters), by design [1][2][3]. 1. Strict Validation: The http crate enforces that a HeaderValue contains only visible ASCII characters (defined as the range 32-127 inclusive, excluding the DEL character 127) [1][4][5]. Because space (0x20) and horizontal tabs (0x09) are either outside this range or classified as non-visible/control characters in the context of this implementation, any input starting or ending with these characters, or containing them, will trigger a validation error [1][3]. 2. Motivation: This behavior is intentional. While HTTP specifications (such as RFC 7230/9110) note that parsers should ignore "Observed Whitespace" (OWS) around field values when extracting them from a raw byte stream, it is considered a requirement that a valid HeaderValue type itself should represent the "sanitized" or "trimmed" value [2][3]. Therefore, the library forces developers to ensure that the data provided to these constructors is already compliant and trimmed of leading/trailing whitespace [2][3]. 3. Historical Context: This behavior was formalized to prevent issues where invalid or ambiguous header values could be constructed [2][3]. Previous discussions and pull requests (e.g., #245, #256) confirmed that it is the responsibility of the library's constructors to validate that the provided string or byte slice adheres to these requirements [2][3]. If you are encountering errors when attempting to create a HeaderValue, ensure that the input string is trimmed of all leading and trailing whitespace and does not contain tabs or other non-printable ASCII characters before passing it to HeaderValue::from_str [1][6][3].

Citations:


🌐 Web query:

rust http 1.4.0 Source code HttpValue v1 header value visible ASCII 9 tab

💡 Result:

In the Rust http crate (version 1.4.0), the HeaderValue type supports the horizontal tab character (ASCII 9, \t) as a valid part of a header value [1][2]. Internally, the crate's validation logic uses a helper function called is_visible_ascii to determine which characters are permitted [1][2]. This function defines visible ASCII characters as those in the range 32–126 (inclusive) and explicitly includes the horizontal tab character (\t, ASCII 9) [1][2]. The code snippet below demonstrates this implementation logic: fn is_visible_ascii(b: u8) -> bool { b >= 32 && b < 127 || b == b'\t' } Consequently, methods that create a HeaderValue (such as from_static or from_str) will successfully accept strings containing horizontal tab characters [1][2]. This aligns with HTTP specifications that permit visible ASCII characters, spaces, and horizontal tabs within header field values [3].

Citations:


🌐 Web query:

http crate 1.4.0 HeaderValue::from_str trailing whitespace control character behavior leading/hard tab not visible ASCII

💡 Result:

In the http crate (including version 1.4.0), the HeaderValue::from_str method enforces strict validation rules on input strings [1][2]. It explicitly permits only visible ASCII characters, defined as the range 32–127 (inclusive) [1][3]. Regarding your specific concerns: 1. Leading and Trailing Whitespace: The http crate prohibits leading and trailing whitespace in HeaderValue objects [4]. This was implemented to align with HTTP specifications (RFC 7230), which expect parsers to ignore whitespace around header values rather than including them as part of the value itself [4]. Attempting to construct a HeaderValue with leading or trailing spaces (ASCII 32 or horizontal tab, ASCII 9) will result in an error [4][2]. 2. Control Characters and Tabs: The implementation restricts HeaderValue to visible ASCII characters (32–127) [1][3]. Because it explicitly excludes characters below 32, control characters—including horizontal tabs (ASCII 9)—are disallowed [1][3]. While some earlier discussions and GitHub issues noted potential nuances regarding how "visible ASCII" is interpreted versus broader HTTP spec requirements (which historically allowed HTAB), the current implementation maintains this strict restriction [1][5]. If you need to include opaque octets (bytes outside the 32–127 range, such as 128–255), you should use HeaderValue::from_bytes instead of from_str [1][2]. Unlike from_str, from_bytes provides a path to construct values with bytes that do not fit the visible ASCII requirement [1][3].

Citations:


Return a failed import item when the bearer token is not a valid HeaderValue.

HeaderValue::from_str(&format!("Bearer {bearer_token}")).unwrap() will panic for tokens containing leading/trailing whitespace or non-visible/control bytes. Since api_key is user-supplied, check the result here and add one failed item with the invalid-token error instead of crashing the import job. .build().unwrap() is comparatively low-risk because client build failures are TLS/layer initialization failures rather than config validation failures.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/services/importer/audiobookshelf/src/lib.rs` around lines 309 - 322,
Update build_client to handle HeaderValue::from_str failure without unwrapping:
propagate the invalid-token error to the importer so it returns one failed
import item instead of panicking. Adjust the function’s return flow and its
caller as needed while preserving normal client creation and existing
client-build behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant