feat(ebook-sidecar): Automatic e-reader delivery, configurable save destination, and series-bundle splitting - #236
Draft
NichCodes wants to merge 8 commits into
Draft
Conversation
…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
marked this pull request as draft
June 30, 2026 02:10
Author
|
Discovered some buggy behavior so I'm converting this back to draft while I test it further. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
Audiobookshelf e-reader device(s).
into a separate ABS library, or a custom path.
1–3" is split into individual per-book requests instead of failing.
Audiobooks / Ebooks / All.
Features
E-reader Auto-send (Audiobookshelf only)
send_to_ereaderjob (delayed + retried so ABS can scan the new filefirst) emails an organized ebook to every requester's enrolled devices via
ABS
POST /emails/send-ebook-to-device.ABS email settings). New
GET /api/admin/settings/ebook/ereader-devices.(
ereader_sent_devices), so retries/re-organizes never duplicate emails and auser who requests the book after it's downloaded still gets it on their
not-yet-sent devices.
hit, to avoid emailing the wrong book.
Ebook Destination Modes
ebook_destination_mode:same(default) |library|custom, with alibrary picker / path field in the Ebook settings tab. Resolved at organize
time with a safe fallback to the default media dir on any misconfiguration.
POST /api/admin/settings/ebook/check-pathvalidates (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
"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.
→ 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/requestsreturns adecomposedresult and the request UI reportsthe split.
Admin Request-type Filter
typequery param onGET /api/admin/requests(validated) and a Typedropdown wired into the table's URL-synced filter bar.
Bug Fix
"http://flaresolverr:8191/" no longer builds "//v1" (which Flaresolverr 404s).
Database
users.ereader_device_names,requests.ereader_sent_devices(schema + migration20260623000000_add_ereader_device_fields).New Config Keys
ebook_ereader_auto_send_enabled,ebook_destination_mode,ebook_destination_library_id,ebook_destination_path.Testing
the
createRequestdecomposed-result contract.localStoragefailures are unrelated tothis branch); production build / typecheck clean.
Notes / Deployment
librarydestination mode writes to the ABS library's own folder path, so theRMAB container must be able to write that path (mount the same host dir ABS
uses).
Audiobookshelf.
filesystem, not the host — e.g. a path is reachable as
/data/media/ebooks(RMAB's mount), not as the host path.
Screenshots