Audiobook support + Readarr-parity UX + migration paths
Import cleanup
- Ebook import no longer leaves the SABnzbd job folder behind. After every book file matches bindery's extension set and moves cleanly, the importer removes the source directory — PAR2, NFO, SFV, NZB, and sample leftovers go with it. Partial-failure runs are untouched so the files remain for investigation.
- Audiobook import handles destination collisions.
UniqueDirresolves{Author}/{Title} ({Year})against the filesystem and appends(2),(3), … when a prior import or manual copy already occupies the slot. PreviouslyMoveDirhard-failed on any collision and the download stuck atCompletedforever. - SABnzbd history is pruned once bindery owns the files. New
DeleteHistory(nzoID, deleteFiles=false)on the SAB client is called after each successful import so completed rows stop accumulating in SAB's UI with stale storage paths. - Remote path mapping (
BINDERY_DOWNLOAD_PATH_REMAP). When SABnzbd and bindery run in separate containers with the shared storage mounted at different paths, SAB would report a completed job at/downloads/complete/Xand bindery would fail to find it under its own mount point — loggingno book files found in downloadand leaving files in SAB's completed dir forever. The new env var accepts comma-separatedfrom:topairs (e.g./downloads:/media), applied longest-prefix-first to each path before the importer walks it. Same-filesystem installs leave it unset and see no behaviour change.
Audiobook support
- Books now carry a
media_type(ebook|audiobook) that drives indexer categories, ranking, library destination, and UI badges. Flip per-book inline on the Wanted page or via the Book detail page. - Search pipeline:
filterCategoriesForMedianarrows indexer queries to the Newznab audio tree (3030) for audiobook books and the books tree (7000 range) for ebooks, with a fallback to the standard category when the indexer's configured set has nothing matching. - Ranking applies a −500 media-type-mismatch penalty and +250 for ASIN exact matches parsed from release titles.
isAudiobookFormatrecognisesm4b/m4a/mp3/flac/ogg. - Import pipeline: audiobook grabs move the entire download directory as one unit via
MoveDir(multi-partm4b/mp3+ cover art + cue sheet stay together) intoBINDERY_AUDIOBOOK_DIR(falls back toBINDERY_LIBRARY_DIRif unset). Naming template defaults to{Author}/{Title} ({Year})— preserves original filenames inside. - Audnex metadata provider (
api.audnex.us, no auth) fetches narrator, duration, cover, and description by ASIN. Endpoint:POST /api/v1/book/{id}/enrich-audiobook. - Release parser extracts Audible-shaped ASINs (
B[0-9A-Z]{9}) from NZB titles;UNABRIDGED/ABRIDGED/RETAILedition flags now factor into ranking. - Raw per-article Usenet postings (
.part09.rar,.vol003+004.par2,.sfv,yEnc,[12/22]brackets) filtered out of search results before ranking so multi-part noise no longer buries clean[M4B]releases.
Readarr-parity UX
- Book and author detail pages at
/book/:idand/author/:id— routed, deep-linkable, replace the previous click-opens-modal flow. Book detail hosts cover, metadata, format toggle, ASIN field, audnex enrich button, inline search-and-grab, and per-book history. Author detail shows portrait + stats + description + Monitored/Refresh/Delete actions + their books as a mini grid. - Grid / Table view toggle on Books and Authors pages (persists per-page in localStorage). Books table: thumbnail + title, author, year, type, status with responsive column hiding on phones. Authors table: avatar + name, book count, rating, Monitored toggle, inline Refresh/Delete.
- History page adds Size and Type columns (desktop table + mobile card) — type auto-detected from the release title's format tokens.
- Books tab: audiobook corner badge on cards; inline
<select>per row on Wanted persists media type viaPUT /api/v1/book/{id}.
Migration paths
POST /api/v1/migrate/csv— upload a newline-separated list of author names or aname,monitored,searchOnAddCSV. Each name resolved via OpenLibrary.POST /api/v1/migrate/readarr— uploadreadarr.db. Authors re-resolved via OpenLibrary (Goodreads IDs aren't portable since bookinfo.club is dead); Indexers / Download clients / Blocklist entries port structurally.bindery migrate csv <path>andbindery migrate readarr <path>CLI subcommands — exit with JSON summary.- Settings → Import tab with file uploads and per-section result cards showing requested / added / skipped / failures.
Infrastructure
developmentbranch joinsmainin CI — builds push:development+:dev-<sha>images and auto-bumpcharts/bindery/values.yaml. Point ArgoCDtargetRevisionatdevelopmentto follow dev builds.- Version badge shows
dev-<sha>on development builds,sha-<sha>on main builds, orv0.4.xon tagged releases. - File download endpoint (
/api/v1/book/{id}/file) now streams a zip when the book'sfile_pathis a directory (audiobook folders come down as a single archive). - Background download-status poll tightened from 60s to 15s so imported status lands in near-real-time after SABnzbd finishes.
- Fixed a
rankResultsbug where precomputed scores were read from stale indices during the in-place sort — composite ranking effectively fell back to indexer-return order. Now zips score with result and sorts pairs. Regression test added.
Added (smaller)
/book/{id}/enrich-audiobookendpoint (audnex).- Foreign-language tag filter now word-boundary-anchored (the tag
RUSSEno longer substring-matches insideRUSSELL). - Book PUT handler accepts
mediaType/asin/narratorfields (was silently dropping them). - Delete downloaded files from the UI. Book detail page gains a red "Delete file" action that wipes the on-disk file (ebook) or folder (audiobook) and flips the book back to
wanted, plus a "Delete book + files" action that removes the record and its files in one go. New endpoints:DELETE /api/v1/book/{id}/fileandDELETE /api/v1/book/{id}?deleteFiles=true. AbookFileDeletedhistory event is recorded so the deletion is auditable. - Skip OpenLibrary "works" whose title equals the author's name. An upstream OL data-quality bug occasionally emits works (e.g.
OL29342228Wfor Jared Diamond) where the Work record was never given a title and the API falls back to the author's name. These polluted the Wanted page and produced nonsense destination folders likeJared M. Diamond/Jared M. Diamond ().FetchAuthorBooksnow filters them out at ingest time and counts the skips in its summary log.