Skip to content

feat(ebook-sidecar): Automatic e-reader delivery, configurable save destination, and series-bundle splitting - #236

Draft
NichCodes wants to merge 8 commits into
kikootwo:mainfrom
NichCodes:feature/ereader-auto-send
Draft

feat(ebook-sidecar): Automatic e-reader delivery, configurable save destination, and series-bundle splitting#236
NichCodes wants to merge 8 commits into
kikootwo:mainfrom
NichCodes:feature/ereader-auto-send

Conversation

@NichCodes

Copy link
Copy Markdown

Ebook Sidecar: Automatic e-reader delivery, configurable save destination, and series-bundle splitting

Summary

Builds out the ebook sidecar into a complete delivery pipeline and adds an admin
request-type filter. Highlights:

  • E-reader auto-send — organized ebooks are emailed to each requester's
    Audiobookshelf e-reader device(s).
  • Ebook destination modes — save ebooks alongside the audiobook (default),
    into a separate ABS library, or a custom path.
  • Series-bundle decomposition — a request for a box set / "Trilogy" / "Books
    1–3" is split into individual per-book requests instead of failing.
  • Admin request-type filter — filter the Request Management table by
    Audiobooks / Ebooks / All.

Features

E-reader Auto-send (Audiobookshelf only)

  • New send_to_ereader job (delayed + retried so ABS can scan the new file
    first) emails an organized ebook to every requester's enrolled devices via
    ABS POST /emails/send-ebook-to-device.
  • Per-user device enrollment in Admin → Users (multi-select sourced from
    ABS email settings). New GET /api/admin/settings/ebook/ereader-devices.
  • Toggle in Admin → Settings → Ebook → E-Reader Delivery.
  • Idempotent: sent device names are recorded on the ebook request
    (ereader_sent_devices), so retries/re-organizes never duplicate emails and a
    user who requests the book after it's downloaded still gets it on their
    not-yet-sent devices.
  • Delivery requires a confident ABS title match — never falls back to a fuzzy
    hit, to avoid emailing the wrong book.

Ebook Destination Modes

  • ebook_destination_mode: same (default) | library | custom, with a
    library picker / path field in the Ebook settings tab. Resolved at organize
    time with a safe fallback to the default media dir on any misconfiguration.
  • Custom-path reachability check — new POST /api/admin/settings/ebook/check-path
    validates (server-side, inside the container) that a custom path exists, is a
    directory, and is writable. The Ebook tab runs it on blur of the path field and
    on save; an amber warning renders under the field when the path isn't reachable
    (with a specific reason: not absolute / missing / not a dir / not writable),
    noting ebooks will fall back to the default media dir. Non-blocking — saving
    still proceeds.

Series-bundle Decomposition

  • Detects bundles via title/seriesPart heuristics (box set, omnibus, trilogy,
    "the complete…", "Books 1–3", plus weak keywords gated by a long runtime) and
    fans out into per-book requests using the Audible series page.
  • Range-aware: explicit ranges ("Books 1–3") and "-logy" keywords ("Trilogy"
    → 1–3) narrow the fan-out; open-ended bundles ("Box Set", "Complete") request
    the whole series. Caps at 30 books; excludes the bundle's own ASIN and nested
    bundles; recursion-guarded.
  • POST /api/requests returns a decomposed result and the request UI reports
    the split.

Admin Request-type Filter

  • New type query param on GET /api/admin/requests (validated) and a Type
    dropdown wired into the table's URL-synced filter bar.

Bug Fix

  • FlareSolverr URL — strip trailing slashes so a configured
    "http://flaresolverr:8191/" no longer builds "//v1" (which Flaresolverr 404s).

Database

  • New nullable JSON columns: users.ereader_device_names,
    requests.ereader_sent_devices (schema + migration
    20260623000000_add_ereader_device_fields).

New Config Keys

ebook_ereader_auto_send_enabled, ebook_destination_mode,
ebook_destination_library_id, ebook_destination_path.

Testing

  • New unit tests: series-bundle detection/enumeration (incl. keyword ranges) and
    the createRequest decomposed-result contract.
  • Full suite passes (pre-existing jsdom localStorage failures are unrelated to
    this branch); production build / typecheck clean.

Notes / Deployment

  • library destination mode writes to the ABS library's own folder path, so the
    RMAB container must be able to write that path (mount the same host dir ABS
    uses).
  • E-reader delivery requires email + e-reader devices configured in
    Audiobookshelf.
  • The custom-path reachability check validates against the container's
    filesystem, not the host — e.g. a path is reachable as /data/media/ebooks
    (RMAB's mount), not as the host path.

Screenshots

New eBook Sidecar settings eReader per-user setting

…splitting

Extend the ebook sidecar into a full delivery pipeline and add an admin
request-type filter.

E-reader auto-send (Audiobookshelf):
- New send_to_ereader job emails organized ebooks to each requester's
  enrolled ABS e-reader device(s); delayed + retried so ABS can scan the
  file first, and idempotent via requests.ereader_sent_devices so retries
  and late requesters never duplicate emails.
- Per-user device enrollment in Admin -> Users, sourced from ABS email
  settings (GET /api/admin/settings/ebook/ereader-devices).
- Delivery requires a confident ABS title match; never falls back to a
  fuzzy hit, to avoid sending the wrong book.

Ebook destination modes:
- ebook_destination_mode = same (default) | library | custom, resolved at
  organize time with a safe fallback to the default media dir.

Series-bundle decomposition:
- Detect box sets / omnibus / "Trilogy" / "Books 1-3" bundles and fan out
  into per-book requests via the Audible series page. Range-aware ("-logy"
  keywords and explicit ranges narrow the fan-out; open-ended bundles
  request the whole series), capped at 30 books and recursion-guarded.
- POST /api/requests returns a decomposed result and the request UI
  reports the split.

Admin request-type filter:
- type query param on GET /api/admin/requests plus a Type dropdown in the
  Request Management table.

Other:
- Strip trailing slashes from the FlareSolverr URL so a configured ".../"
  no longer builds "//v1" (which byparr 404s).

Schema: add users.ereader_device_names and requests.ereader_sent_devices
(nullable JSON) with migration.

Config: ebook_ereader_auto_send_enabled, ebook_destination_mode,
ebook_destination_library_id, ebook_destination_path.
Bundle detection still runs inline in createRequestForUser, but the
fan-out — enumerating the series and creating up to MAX_BUNDLE_BOOKS
per-book requests (each an Audnexus lookup + notification + search
enqueue) — now runs in a new `decompose_bundle` job instead of blocking
POST /api/requests. Large box sets no longer risk a client timeout.

The request returns a `decomposed` result immediately (count 0 / books
[] with a "splitting in the background" message); the per-book requests
appear shortly after as the job completes. The job preserves the prior
fallbacks: enqueue failure or an unscrapeable series both fall back to
creating the bundle as a single normal request.
…k safely

The custom ebook destination previously returned the configured path
verbatim, so an unreachable/unwritable path failed organization instead
of falling back to the default media dir as documented — the save-time
check-path call is only advisory and config can drift.

resolveEbookDestinationDir() now re-verifies a custom path at organize
time and falls back to the default media dir when it isn't usable. The
reachability logic is extracted into a shared checkPathReachable() helper
so the organizer and the check-path admin API agree on what "reachable"
means (eliminating the duplicated fs.stat/access logic).
Search jobs set the request to `searching` as their first action, but a
Bull stall or event-loop freeze bypasses the processor's catch and
orphans the request forever — no scheduler rescans that status.

Root cause: a midnight thundering herd (5 daily jobs all at 0 0) over a
single byparr solver froze the event loop ~5 min, expiring every Bull
job's default 30s lock at once.

Recovery:
- job-queue `failed` handler resets searching → awaiting_search when a
  search_ebook/search_indexers job permanently fails
- retry_missing_torrents reaps requests stuck in `searching` past 30 min

Prevention:
- Bull settings: lockDuration 120s, stalledInterval 60s, maxStalledCount 2
- search_ebook concurrency 2 → 1 (serialize against single byparr)
- Stagger daily crons: audible 0:00, watched 1:00, retry 2:00, ebooks
  3:00 + migrateLegacyMidnightSchedules() for existing installs
… searches

After 3 consecutive no-result searches, ebook requests now settle as
`unavailable` instead of cycling in `awaiting_search` forever. The
status shows a slate "Unavailable" badge and stops the daily retry
churn, while still allowing recovery:

- Manual Search / Interactive Search available from the admin dropdown
- Weekly `retry_unavailable_ebooks` job (Sun 04:00) resets searchAttempts
  and re-queues all unavailable ebooks for a fresh search cycle
- find_missing_ebooks skips `unavailable` children (won't re-create them)
- Requests can also be cancelled from the `unavailable` state
Add a new "Search Anna's Archive" modal with structured search fields
(title, author, ASIN/ISBN, format, year, free text) that returns
multiple results with cover thumbnails for user selection. Searches use
direct HTTP (no FlareSolverr needed), deferring FlareSolverr to
download time.

- New API route POST /api/requests/[id]/search-annas-archive
- New searchAnnasArchiveMulti() in ebook-scraper with AA HTML parsing
- select-ebook resolves download URLs on-demand when only md5 provided
- Ebook 3-dot menu: "Search Anna's Archive" replaces "Manual Search",
  "Search Indexers" replaces "Interactive Search" (gated by
  indexerSearchEnabled), "Adjust Search Terms" removed for ebooks
- "unavailable" added to reusable-status guards in select-ebook and
  interactive-search-ebook routes
- StatusBadge: "Awaiting Search" now orange with whitespace-nowrap;
  "Downloaded" shows as "Available" for ebooks
- Active downloads progress bar: pulsing animation at 0%, 700ms
  transition for intermediate values, wider bar
- direct-download processor throws on failure so Bull marks job failed
  (was returning success:false → green "Complete" in logs)
- Job startedAt clears completedAt on retry (fixes negative duration)
- formatDuration clamped to non-negative
@NichCodes
NichCodes marked this pull request as draft June 30, 2026 02:10
@NichCodes

Copy link
Copy Markdown
Author

Discovered some buggy behavior so I'm converting this back to draft while I test it further.

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