Releases: vavallee/bindery
Release list
v1.32.2
Books you already owned kept turning back up in Wanted. Three separate
faults produced the same symptom, and this release fixes all three. An
Audiobookshelf import read an item listed without its files as an audiobook and
quietly made ebook only books dual format, so they wanted an audiobook that was
never real. An author refresh did the same thing from the other direction,
widening any owned book whose work a catalogue also listed in the other format:
one reporter had a single routine refresh flip 29 already downloaded books, some
added two months earlier. And a file that moved on disk left the book rendering
the old, dead path while still claiming to be imported, which no amount of
rescanning corrected. In each case the book's status is derived from the formats
still missing, so an invented want was enough to drop it off the shelf.
Heavily reprinted books were disappearing from libraries entirely. Bindery
read only the first 50 editions of an OpenLibrary work, in an order OpenLibrary
does not sort, and the Min pages and Skip missing ISBN metadata profile settings
decide from that list. A title whose qualifying edition happened to sit further
down was skipped on every refresh. One work in testing had 139 editions, 131 of
them carrying an ISBN, more than half of the page counts past the cut.
Path remaps were impossible to configure with the download client on
Windows. A remap written with a drive letter was split at the drive letter's
own colon, so it silently matched nothing and the Settings validator accepted it
anyway. Torrents were added, author and book folders were created, and no file
was ever copied. Anyone running qBittorrent on Windows with Bindery in Docker
had no working form to write.
A co-author could swallow another author's whole catalogue. Importing a
Calibre book credited to two people made the second one an alias of the first,
so every later book by that co-author was filed under their collaborator and
they never got an author page. Which author survived came down to Calibre book
id order, which is why the effect looked arbitrary. Libraries already affected
recover on the next import.
Quality profiles were never checked against the file you actually received,
only against the release name, and the check deliberately let through any
release whose title carried no recognisable format. So the one case it admitted
it could not judge was exactly the case that arrived unchecked.
Download client health is now checked for every client type. If a client
turns red in Settings after this update, that is almost certainly a problem that
was already there. Nothing about your setup changed; Bindery just started
looking. Only qBittorrent was ever checked before, and the other five stored a
green result without checking anything.
Changed
- Author refresh fetches Hardcover editions for new books a few at a time instead of one after another (#1929). After the library walk fix in v1.30.3, edition hydration was the last provider call the sync made once per newly added book, waiting for each one to come back before starting the next. A 65 book author paid 65 round trips in sequence. The sync now knows the full set of books it created before hydrating any of them, so it fetches their editions in parallel and hands the results to the same hydration path as before. A refresh that adds nothing still fetches nothing, and a lookup that fails is retried live exactly as it was.
Fixed
-
An Audiobookshelf import no longer sends owned books back to Wanted (#2169). When ABS listed an item without its files, because the per item detail fetch was skipped or failed, Bindery read that silence as "audiobook" and widened the book to dual format. Status is derived from the formats still missing, so the book then wanted an audiobook that never existed, and the next import demoted it from Imported back to Wanted with its ebook still on disk and still attached. Re-importing repeated the demotion instead of repairing it; one reporter had 23 of 51 rows in that state. Only the formats an item actually exposes now take part in the merge. Books already widened keep their media type, so set one back to the format you want and Bindery re-derives the status.
-
An author refresh no longer changes the format of a book you already own (#2096). When a catalogue listed the same work in the other format, the refresh merged the row to dual format whether or not the book was already on disk, which invented a want nobody asked for. The merge still runs for a book nobody has yet, which is the duplicate row problem it exists to solve. Books already widened keep their media type; set one back by hand and the status follows.
-
A moved book file left the book pointing at a path that was gone (#2186). After a file moved on disk, a scan registered it at its new location but the book kept showing the old, dead path while still reporting Imported, and no amount of rescanning corrected it. A book now shows whichever of its tracked files still exists on disk, and a library scan repairs books that were already stuck this way. The entry for the old location is not deleted: it stays listed under the book's Files, where Forget this file clears it without touching anything on disk.
-
Heavily reprinted books no longer go missing from an author's library (#1779). Bindery read only the first 50 editions of an OpenLibrary work, in an order OpenLibrary does not sort. The Min pages and Skip missing ISBN metadata profile settings decide from that list, so a title whose page count or ISBN happened to sit further down was skipped on refresh even though it qualified. The full edition list is now read, so those books come back on the next author refresh.
-
Path remaps now work when the download client runs on Windows (#2191, reported in Discussion #1971). A remap written with a drive letter, such as
S:\Downloads:/mnt/Storage/Downloads, was split at the drive letter's own colon and quietly did nothing, so nobody running qBittorrent on Windows with Bindery on Linux or Docker could configure a working remap at all. Drive letters are now understood on both sides of the pair, either slash works on the Windows side, matching there ignores case while Linux paths stay case sensitive, and the result comes back as a clean path for whichever platform it lands on. Settings now rejects a drive path with no destination instead of silently accepting it, and the download client Test button explains that a Windows client requires a remap rather than suggesting an identical mount that cannot exist. -
Calibre import no longer files co-authors as aliases of the primary author (#1684). A book credited to two people made the second one an alias of the first, so every later book by that co-author was filed under their collaborator and they never got an author page at all. Co-authors are simply not recorded now, since a Bindery book carries one author, and an import ignores an existing alias unless something backs it up, so libraries already affected recover on the next import. No alias rows are deleted, and author merges, spelling variants and non-latin alternate names keep resolving exactly as before. Leftover rows can be removed from the author page if you want them gone.
-
The Calibre library import now tracks the files it imports (#1635). It reconciled titles, authors and series from
metadata.dbbut never recorded where the files actually were, so a Calibre managed book was marked imported while Bindery tracked no file for it. Every format Calibre reports is now tracked, so an epub and an audiobook of the same book are both attached rather than one hiding the other, and re-running an import does not duplicate rows. This also gives Calibre libraries a supported way to correct a file path, which they previously lacked. -
A quality profile's allowed formats are now checked against the file you actually got (#1782). The check ran once, before downloading, against the release name, and deliberately let through any release whose title carried no recognisable format, which is common for Usenet posts named like "Author - Title (Year)". Nothing after that ever looked again, so a release that turned out to be a MOBI, with no file extension at all, imported cleanly into a library whose profile disallowed MOBI. Bindery now identifies the real format from the file's contents before importing it. A download whose files are all disallowed is held for review rather than imported, with the reason attached, and the release is blocklisted so the next search does not simply grab it again. A download containing a mix imports the format you allowed and leaves the rest. Manual imports are unaffected: if you pick the format yourself, that is the answer.
-
Download client health is now checked for every client type (#2029). Previously only qBittorrent was ever checked. SABnzbd, NZBGet, Transmission, Deluge and rTorrent each stored a green "path check not required" without checking anything, and the Settings page would not have displayed the result even if they had. That is the mechanism behind the most common support report we get: the connection tests fine, the client accepts grabs, and nothing ever imports. Health is also re-checked every 15 minutes now, so a client that breaks after setup, a remounted seedbox or a rotated password, no longer stays green until someone thinks to press Test. The three client types that genuinely cannot be introspected say so plainly instead of claiming to be fine.
-
Download clients that refuse an action are no longer treated as having done it (#2192). SABnzbd and NZBGet report a refused pause, resume, delete or history delete as an ordinary HTTP 200 with a failure flag in the body, and Bindery read only the HTTP status. The most visible effect was post import history cleanup: SABnzbd kept the finished job in its history, and the "cleanup failed" warning that was supposed to...
v1.32.1
Single sign-on could lock you out of your own instance.
Bindery read OIDC group membership only from the ID token and never called the
provider's userinfo endpoint. Authelia, Okta and Auth0 do not put groups in
the ID token by default and serve it only from userinfo, so Bindery saw no
groups at all. With BINDERY_OIDC_ADMIN_GROUP set, that empty result read as
"not an admin" and demoted the user on every login, including the operator who
configured it. The last-admin guard is deliberately bypassed on this path, so
there was nothing to catch it. It also explains OIDC users whose Bindery
username was a sub UUID, since preferred_username is userinfo-only under
the same defaults. Bindery now reads userinfo as well and merges it underneath
the ID token, and a group claim missing from both leaves your existing role
alone instead of assuming the worst.
Adding the second volume of a series did nothing. With Hardcover as the
metadata provider, adding a book whose main title matched a volume already in
your library was silently folded onto that volume. The Add button flickered, no
book appeared, and no error was shown. Bindery has a guard for exactly this,
comparing the two volumes' series positions, but the query that fetches a
single book never asked Hardcover for its series, so the guard had nothing to
compare and waved the merge through every time.
Migrating from Readarr turned every download client into SABnzbd, unless it
was qBittorrent. A Readarr install on Transmission, Deluge, NZBGet or rTorrent
imported a client with the right host and port and entirely the wrong type. It
saved, it appeared in the list, and then every grab against it failed, with
nothing in the migration result admitting the type had been guessed.
Bulk folder import timed out and returned nothing. The scan matched each
file against the catalogue one at a time, at roughly 213 ms per file on network
or spinning storage, so a thousand file scan ran past three minutes, outlived
the server's write timeout, and died mid response. The page sat there and then
retried straight into the same wall. That scan now finishes in under half a
minute.
Changed
-
SQLite now runs the connection pragmas that WAL expects (#2142). Turning on WAL never changed
synchronous, so every commit was still paying a disk flush under SQLite's FULL default, and the page cache and temp store were left at their untuned defaults. Writes on the import, scan and download poll paths get noticeably cheaper. The trade, stated plainly: withsynchronous=NORMALan OS crash or power loss can lose the last few committed transactions that have not been checkpointed. It cannot corrupt the database, and an application crash loses nothing. See Database durability. -
Audiobookshelf imports fetch Hardcover series catalogues concurrently (#2144). A search returns up to five candidate series, and each one's catalogue was fetched only after the previous had returned, so a book whose series were not yet cached cost five sequential round trips. Most noticeable on the first import of a library, which is when the fewest series are cached.
-
Calibre cover downloads reuse one HTTP client (#2144). Each cover fetch built its own, so two covers from the same host could not share a connection. Affects the first import of a library, before the on disk cover cache is warm.
-
make checknow runsgovulncheck(#2140), pinned to the same revision CI installs.CONTRIBUTING.mddescribed the target as running what the gating CI checks run, and listed the vulnerability scan among them, but the target left it out. A contributor who ranmake checkbefore opening a pull request had not actually run the scan CI would fail them on. -
Contributor documentation corrected and extended.
ARCHITECTURE.mdclaimed SQLite reads run concurrently, which the single connection pool has never allowed.CONTRIBUTING.mdgains a guide to adding a metadata provider, download client or indexer, the three likeliest outside contributions and previously the three least documented.
Fixed
-
OIDC group mapping works with Authelia, Okta and Auth0 again, and a missing group claim no longer takes your admin rights away (#2097). Bindery now reads the userinfo document as well and merges it underneath the ID token, so a claim carried by both keeps the signed ID token's value, and a userinfo document whose subject does not match the ID token's is discarded (OIDC Core 1.0 §5.3.2). A group claim missing from both leaves the existing role untouched and logs why; a claim that is present and does not list the admin group still demotes, because that is the provider actually saying so.
allowed_groupsnow says which of the two it rejected a login for instead of giving the same message either way. -
Adding a second volume of a series did nothing (#2116). The query that fetches a single book never asked Hardcover for its series, so the duplicate guard had nothing to compare and always let the merge through. The same field was missing from the ISBN lookup.
-
Hardcover supplemented author catalogues had no series information (#2121). Works pulled from Hardcover to fill out an author's bibliography arrived with no series membership, so those books were never linked into a series and an author set to monitor a specific series never picked them up. They now carry the series and volume number Hardcover holds. Existing books gain their links on the next author refresh.
-
Readarr migration turned every non qBittorrent download client into SABnzbd (#1983). All six client types Bindery supports are now mapped by name, and a Readarr client with no Bindery equivalent, such as NZBVortex or a blackhole, is reported as a skipped row instead of becoming a SABnzbd client that cannot work.
-
Bulk folder import scan timed out and returned nothing (#1638). Matching now runs up to eight files at once.
-
Import lists ignored their root folder (#1864). The per-list root folder picker saved its value and the Hardcover list syncer never read it, so authors created by a list landed with no root folder at all. Same defect as the quality profile in #1781, one field over. A list with no root folder configured still leaves the author's unset rather than guessing, because root folders belong to a user and picking one for somebody else's authors is worse than leaving it empty.
-
Author imports kept creating rows with no metadata profile (#1803). Six author creation paths, including ABS import, the Calibre importer, the Goodreads migration and the CSV importer, inserted authors with the metadata profile column empty. v1.31.0's migration cleaned up the ones that had accumulated, but the paths producing them were untouched, so the next import started refilling it. Nothing behaved differently in the meantime, since every reader already fell back to the default; the profile column in the author UI was simply blank when it should not have been.
-
Grabs signed by host match lost their indexer (#2053). When an API client posts a release with no indexer id, Bindery works out which configured indexer the download URL belongs to and signs it with that indexer's key. It never recorded which one it picked, so the download row had no indexer attribution in the queue and any per indexer seed ratio override was skipped. Two indexers sharing a host and key still sign the URL but attribute nothing, since their seed ratios can differ and choosing between them would be a guess.
-
"SABnzbd rejected download" never said why (#2120). SAB replies to a refused upload with its own explanation, and Bindery decoded only the success flag and discarded the reason. Since SAB deletes the uploaded file before its own backup step, that reply was the last place the explanation existed. The reason now reaches the queue and the history entry.
-
Audiobookshelf base URL rejected a port with an error about the scheme (#2056). Typing
audiobookshelf:13378, the natural form when everything runs under Compose on one host, produced "must use http or https". Go reads a scheme-lesshost:portas a scheme, so the complaint pointed at the wrong half of the input, and the obvious next move was to drop the port and land on port 80. Ports were always supported. The error now names the missing scheme and prints the exact value to use. -
Strict media type never said it was letting explicit adds through (#1759). The setting skips catalogue books in the wrong format and narrows dual format ones, but a book you add yourself has always been created in the format you picked, on both add paths. That is deliberate, since silently refusing something you explicitly asked for is worse than the row it prevents, but nothing said so anywhere. The setting's help text now states the boundary, and an add that goes past the policy is logged instead of passing unremarked.
Removed
- Three scratch files that were committed by accident:
err.log,magazine-feature-prompt.txtandui-browser-test-prompt.txt. They shipped in every clone. Their siblings were already ignored; these were missed.
Security
-
The login rate limiter no longer grows without bound (#2137). The per address bucket map only expired entries for an address that came back, so a caller rotating source addresses, which is trivial inside a single IPv6 /64, could grow it indefinitely. Buckets are now swept once per window. Forged
X-Forwarded-Forwas never the vector here, since forwarded headers are stripped from untrusted peers. -
The telemetry server compares its stats token in constant time (#2138). The
/api/statsand/api/backupbearer check used a plain string comparison, whose timing leaks how much of a guess was correct./api/backupreturns a snapshot of the installs database, so that token is the only control in front of it. Every other secret in the main binary was already comp...
v1.32.0
Five metadata profile filters did nothing at all. skipPartBooks,
skipMissingDate, minPopularity, minPages and skipMissingIsbn each had a
control in the UI, each saved its value, and not one of them was ever consulted
while cataloguing an author. Setting them changed nothing, and there was no way
to tell from the outside — the books they should have excluded simply arrived
anyway. All five now filter, and the author page reports what each one dropped
with sample titles, so a profile that is too aggressive is visible rather than
something you infer from a book that never showed up. Books already in your
library are exempt from every one of them: an owned book still reaches the
update path and keeps getting its ratings, genres and cover refreshed, so
turning a filter on cannot strip metadata from what you already have.
A clean install with no Hardcover token hammered Hardcover anyway. A bulk
CSV import of nineteen authors fired 447 Hardcover requests, of which 240 came
back "unable to verify token" and the remaining 205 were throttled. Hardcover
authenticates every query, search included, so not one of them could have
succeeded. Only one of the client's queries checked for a token first. All of
them do now, and an install without a token makes no Hardcover requests at all.
Startup used to log hardcover enrichment enabled regardless; it now says
idle: no api token configured when that is the truth.
And CSVs saved by Excel, Google Sheets or Numbers imported a stranger's
library. Those apps write a byte order mark at the start of the file, and it
stuck to the first cell — so the header row stopped looking like a header, got
imported as an author name, matched some unrelated person on OpenLibrary, and
pulled in their entire catalogue. That is most of the rest of the request
volume above, plus a bogus author in your library. Goodreads exports failed
differently and more honestly: the mark made the Title column unfindable and
the import refused to run.
Upgrading from a version before 1.15 could fail to start, with
no such column: books.excluded. A column added years ago never landed on
some databases, and every startup after that died on the first query touching
it. It is restored automatically now.
Deleting one format no longer takes the other one with it. Audiobook
imports register the destination folder, so a ?format=audiobook delete
landed on a directory and removed all of it — including the ebook sitting
beside the audio files. The ebook's database row survived, so the book went on
advertising a file that was no longer there.
Added
-
Bulk media-type editing gained a "Set Both" action, and works from the author page (#2066) — the Books view's bulk bar could set Ebook or Audiobook but never the pair, because
POST /book/bulkrejectedmediaType: "both"even though the author-level bulk action accepted it and the underlying write is media-type agnostic. A batch of books owned in both formats had to be corrected one at a time from each book's edit page.bothis now accepted, 📖🎧 Set Both sits alongside the existing two buttons, and all three are also available on an author page's book list — where a media-type correction after a bad sync usually starts, and where the previous workaround (filter the global Books view by author) cost the author-scoped context the cleanup needs. -
Author sync reports what each filter dropped, with examples (#2032) —
AuthorSyncSummarycarries a count and a sample of titles for every skip reason, and the author page's sync notice lists them. Previously a metadata profile could silently discard most of an author's catalogue and the only evidence was a shorter book list than you expected.
Changed
-
Books that share a main title are no longer merged (#2042) — the canonical title key used to stop at the first
": ", soStar Wars: A New HopeandStar Wars: The Empire Strikes Backwere one identity and an import could bind one onto the other. Distinct subtitles now mean distinct books; a subtitle only one source spells out still matches. Keys are recomputed automatically on the next start, so no action is needed; libraries already holding duplicates keep them, as merging existing rows remains a separate piece of work. -
Series Fill picks the best title match rather than the first (#1969) — Fill now scans every existing candidate title and links the best-scoring one, not just the first past the confidence threshold. On a library where two different real books both clear the threshold against one catalogue title, the book Fill links can change from "whichever came first" to "whichever scores higher".
Fixed
-
The five metadata profile filters that never filtered (#1723) —
skipPartBooks,skipMissingDate,minPopularity,minPagesandskipMissingIsbnwere each persisted and each ignored during cataloguing. All five now apply during author sync and refresh:- Part books removes box sets, omnibuses, signed-copy cartons and slash-separated multi-title anthologies.
- Missing date removes works with no release date.
- Minimum popularity removes works below the ratings-count floor, exempting works that have not released yet and so cannot have accumulated any.
- Minimum pages and missing ISBN are checked against a real edition lookup. A work with no page data at all counts as unknown rather than zero and passes the floor either way.
The edition lookup runs only for works that survived every free filter, only when one of those two settings is on, and is batched across the survivors with bounded concurrency rather than fired one at a time — so a profile touching neither setting pays nothing, and one that enables them does not add a serial round-trip to author sync (#1929). Books already in your library are exempt from all five and keep receiving rating, genre and cover updates.
-
Hardcover is no longer queried when no API token is configured (#2075) — a bulk CSV author import on a fresh config fired hundreds of Hardcover requests that came back
401 Unable to verify tokenand then429 Throttled, because only one of the client's queries checked for a token first. Every Hardcover query now short-circuits as "not configured" before any network call. Adding a token still takes effect immediately, with no restart. -
The Hardcover line in the startup log is honest (#2075) — startup always logged
hardcover enrichment enabled, even with no token configured. It now logshardcover enrichment idle: no api token configuredin that case. -
CSV imports of files saved by Excel, Google Sheets or Numbers (#2075) — those apps put a byte order mark at the start of a UTF-8 CSV, and it used to stick to the first cell. The header row was no longer recognised as a header, so it was imported as if it were an author name, matched an unrelated person on OpenLibrary, and pulled in that stranger's whole catalogue — often hundreds of provider requests, rate limiting and a bogus author in your library. The mark is now stripped before parsing, for author CSVs (both the two-column and the plain name-per-line form) and for Goodreads exports, where it made the Title column unfindable and the import fail outright.
-
Upgrades from before 1.15 no longer die on a missing column (#1932) — the legacy-missing
books.excludedcolumn is restored automatically at startup instead of failing every query that touches it. -
Deleting one format no longer destroys the other format's file when both live in the same folder (#2052) — audiobook imports register the destination folder in
book_files, so a?format=audiobookdelete landed on a directory. That branch was an unconditionalos.RemoveAll, which discarded the format filter every other part of the delete path honours, and took the ebook sitting beside the audio files with it. The ebook'sbook_filesrow survived, so the book kept advertising a file that was no longer on disk and downloading it returned an error. The directory branch now walks the folder and removes only the files belonging to the format being deleted; cover art and other sidecars are removed once no book file of any format is left, and the folder itself only when nothing remains in it. -
A folder delete can no longer unlink a file another book still tracks (#1368, surfaced by #2052) — the
book_filesownership guard was applied only to the tracked path handed to the delete, so any file nested under a deleted folder was unguarded. The same check now runs for every file the sweep reaches, including same-stem siblings. -
Bulk "Set monitor mode" can now set whether authors accept newly discovered books (#2065) — the dialog wrote
monitor_modeand nothing else, so setting a whole library to None left every author still pulling in its back-catalogue on the next refresh. The two settings are independent, and the field was only reachable from the single-author edit form. The bulk dialog now carries a Monitor newly discovered books control alongside monitor mode, defaulting to Leave unchanged so an existing bulk action behaves as before. Mode None still does not imply it: that pairing is the supported "list the whole catalogue, monitor none of it" setup. -
Library scan no longer rejects audiobooks when
BINDERY_AUDIOBOOK_DIRdiffers from the ebook root (#2033) — the scan's reconcile tiers matched a file by ASIN, fuzzy title, or series position and then checked the candidate against the ebook root regardless of the file's format. With a separate audiobook root, every correctly matched audiobook failed that containment check and fell through to the genericno_title_matchreason, so the scan reported that it could not identify files it had in fact identified. Each file is now checked against the root for its own format. -
Long titles and long authors together no longer fail the import with "file name too long" (#2014) — the per-value cap add...
v1.31.0
What's new
rTorrent and ruTorrent are supported (#1618) — a first-class download client alongside qBittorrent, Transmission and Deluge. Works over the HTTP XML-RPC endpoint ruTorrent and seedbox panels expose, and over rTorrent's own SCGI socket for plain installs (scgi://host:port or scgi:///path/to/socket). Grabbing, queue status, labels, import, removal, and a Test button that checks whether Bindery can actually read where rTorrent writes.
Download your logs from the UI (#1903) — a Download button in Settings → Logs saves the filtered entries as a text file, keys and tokens stripped. No container shell needed to attach logs to a bug report.
Jump to any page (#2010) — the … in the pager is a dropdown now instead of decoration. Page 30 of 40 is one click.
Set the Hardcover sync interval (#1848) — 1 hour to 7 days, instead of a hardcoded 24.
Hardlink warns before it silently copies (#1720) — picking Hardlink when your download folder and library are on different filesystems now says so inline, where you make the choice.
Fixes worth knowing about
Dual-format folders attach both files in one scan (#1957, #1956, #1885) — a folder with an epub and an m4b used to attach one and leave the other Unmatched until you scanned again. And an m4b whose Artist tag carries a contributor list (author + translator + narrator, which is how Audible tags things) could never match at all, on any scan, forever. Both fixed. When a file still doesn't match, the Unmatched table now tells you why per file.
Downloads stop failing while they are still downloading (#1884) — a fresh torrent reports 100% for a moment, and that was enough to trigger an import against a folder that didn't exist yet. Three retries burned through in 45 seconds and the download was dead, blaming your path mapping. Retries no longer count when there is nothing on disk, a stuck download ends up visibly blocked with the real reason, and a blocked download releases its release so you can grab it again.
Adding one book adds one book (#1816, #1815) — Add Book used to pull in the author's entire bibliography behind it (75 books became 500 for one reporter), and a refresh did the same even with monitoring off. Ships one schema migration; it applies at startup.
Hardcover "Sync now" finishes (#1854) — it ran inside the web request and the 60-second timeout cut it off, so a 1,660-book shelf imported about a third and lost the rest silently. It's a background job now with progress on the import list row.
Long non-ASCII titles import (#1982) — a Japanese, Chinese, Korean, Russian or Greek title of ~83 characters could fail with "file name too long". The limit counted characters; filesystems count bytes.
OPDS readers fetch covers from Bindery (#2020) — KOReader and friends were hot-linking the metadata provider's CDN. Covers now come from your instance's local cache.
Security and licensing
A malicious torrent file could crash Bindery. The bencode walk that reads a torrent's infohash had no recursion limit, so a crafted .torrent from any indexer could exhaust the stack and take the process down, repeatedly. Nesting is now capped.
Go 1.26.6, picking up six standard library advisories.
A GPL-3.0 dependency was removed from the binaries (#1988). Bindery is MIT, Go links statically, so a GPL fuzzy-matching library made every published binary a combined work that couldn't honestly be offered under MIT. Replaced with a first-party implementation producing the same scores — 19,306 real title pairs checked, no matching decision changed. Every release now also ships THIRD_PARTY_LICENSES.md (#1989).
Background jobs no longer take the process down when they panic (#1967).
Full detail for every change is in CHANGELOG.md.
v1.30.4
One fix, and it is the reason to upgrade straight away: v1.30.1 through v1.30.3
could leave Bindery unable to start at all, on every restart, with no way back
in.
Migration 72 rebuilds two Calibre tables, and rebuilding a table in SQLite means
turning foreign-key enforcement off for the duration — so the migration runner
verifies referential integrity before it commits. That part is right. What was
wrong is the scope of the check: it asked SQLite about the entire database
rather than the two tables the migration had just rebuilt. Any orphaned row
anywhere, including drift that predated the migration by months, failed it.
That drift had a source. Until #1727, connection-pool replacements ran without
foreign_keys set, so the ON DELETE CASCADE rules the schema declares quietly
stopped firing on long-running instances and orphan rows accumulated. #1727
stopped new drift but never cleaned up what had already collected, and migration
72 is where the bill arrived. The unpleasant part is who it selected for: the
longer an instance had been running, the more orphans it carried, and the more
certain it was to fail. The oldest and largest libraries were the most exposed.
Nobody lost data. The migration runs inside a transaction and rolled back every
time, so affected databases are intact — they simply could not be opened by
v1.30.1 or later. This release opens them, and adds two offline commands for
inspecting and clearing the leftover drift on an instance that cannot start.
Fixed
- Bindery would not start after upgrading to v1.30.1–v1.30.3
(#1972,
#1974) — the instance exited
on every restart withmigration 72: foreign_key_check found N violation(s).
Migration 72's integrity check scanned the whole database instead of the two
Calibre tables it rebuilt, so orphan rows left over from the pre-#1727
foreign-key drift — accumulated long before that migration existed — aborted
the upgrade. Longer-running instances were the most likely to hit it. The
runner now compares per-table violation counts from before and after the
migration and fails only on violations the migration itself introduced;
pre-existing ones are logged with their table names and counts and no longer
stop the instance. A migration that genuinely corrupts what it rebuilds still
aborts and rolls back, as before.
Added
-
bindery db-checkandbindery db-repair
(#1972) — offline database
integrity tooling that runs without applying migrations, so it works on an
instance that cannot start.db-checklists every row whose foreign key points
at a missing parent and changes nothing;db-repair --yesreplays the delete
rule the schema declares (ON DELETE CASCADErows removed,ON DELETE SET NULLreferences cleared, anything else skipped and reported) and prints what
it did.--yesis required; without it the command refuses and names the file
to back up. Also reachable asBINDERY_DB_FK_CHECK=report|repairfor setups
where the container command cannot be edited. See
Troubleshooting.docker run --rm -v bindery-config:/config ghcr.io/vavallee/bindery:latest db-check docker run --rm -v bindery-config:/config ghcr.io/vavallee/bindery:latest db-repair --yes
v1.30.3
Six fixes. Three of them are one story: a book's media type records what Bindery
was told to go and fetch, but the book detail page was using it to decide what
to show you about the files already on disk. A book can legally hold both an
ebook and an audiobook, and when the two disagreed the page put an Audiobook
badge next to an epub's path, gave the second file no surface at all, and —
the part worth upgrading for — offered a Delete button that removed both
formats while naming one of them. The File section is now a list of what is
actually there, and every delete says exactly which paths it will remove.
The other three are things Bindery was doing repeatedly and silently: work it
had already done, requests it had already been refused, and a field it was
confidently reporting wrong.
The one most likely to be felt is the author refresh. Before queuing a search
for each book it creates, the sync checked whether the file was already on disk,
and that check walked the entire library — once per book. On local disk the OS
caches the directory tree and it hides; on a NAS mount it does not, and a
65-book refresh spent close to an hour doing nothing but re-reading the same
directories. It now walks each library root once per refresh.
The other two came from users noticing something odd and looking closer. An
indexer that answers "request limit reached, retry in 485 minutes" was being
asked again on the very next search, and every search for the following eight
hours, because nothing recorded what it had said. And a book whose file was
Spanish displayed as English forever, because the language shown came from the
metadata provider's description of the work and the file's own tag was only
ever read when the provider had supplied nothing.
Fixed
-
The book detail page now lists every file on the book, and a delete can no
longer remove a format you were not shown
(#1948) —media_type
records acquisition intent, what search and monitoring are told to hunt for.
book_filesrecords inventory, what is on disk. The File section rendered
inventory through the intent value, so any file outside the declared type was
invisible to display but still included in destruction. A book marked
audiobookthat also held an epub showed a 🎧 Audiobook badge (from the media
type) next to the epub's path (from the legacyfile_pathcolumn, which was
kept ebook-first regardless of media type); the audiobook itself had no row at
all, because the format switcher only appeared for books already marked
dual-format; Download sent no format and served the epub; and Delete file sent
a format-lessDELETE, which enumerates every registered file and removes
both formats, while the dialog named one path and described it as the other
format's. The badge and the path came from different sources, and the
confirmation and the request disagreed.The section is now a list built from the files themselves, grouped by format,
each group badged by its own format and nothing hidden behind the declared
type. Download and Delete live on the format group and are always scoped to
it, which is the honest unit: both endpoints act on every file of that format,
plus, for delete, the same-name sibling sweep. The confirmation dialog lists
every path the request will remove and is built from the same state the
request is, so the two cannot drift apart again. A format-less delete is still
available but only as an explicit Delete all files action, and its dialog
lists every path across both formats. Fix match now moves the file whose
row you opened rather than whichever format the switcher was on, and the
switcher is gone: hiding one format behind it is what made a registered file
invisible in the first place. One thing the switcher did carry is kept: a
format the book wants but has no file for still shows as Not downloaded,
so a dual-format book with one file on hand says which half it is still
waiting on. Books that predate thebook_filesmigration and were never
re-imported still render from the legacy columns, and because that legacy
single path carries no format of its own, its Download and Delete deal with
it as the book's only file rather than guessing a format the server might
disagree with.Two smaller things fell out of the same work. A new per-file Forget this
file action drops a stale path from Bindery's records without touching disk
— the database-only mode added in
#1692 had no interface at
all until now, which is what you want when a file has already been moved or
removed elsewhere and the old path is still being reported. And the media
badge was a two-way ebook/audiobook check, so a dual-format book displayed as
"📖 Ebook"; it now renders both. -
A book holding both formats now declares itself dual-format
(#1946) — the display fix
above makes the page correct whatever the media type says, but the media type
was also simply wrong, and it is what search and monitoring read. When both an
ebook and an audiobook are registered against a book, its media type is now
widened tobothon the next file event, because a file on disk settles the
question of what the book is. This is driven by inventory and is deliberately
independent of the metadata-driven widening pinned in
#1732: that pin exists
because Hardcover lists an audio edition for most popular titles, so widening
from metadata alone was widening on a claim. Here the audiobook is already
imported. Widening only ever fires when both files are present, so it cannot
flip a book back to wanted or start a download. Affected books heal on their
next import, delete, rename, or library reorganize; nothing runs at upgrade.Two related corrections ride along. The legacy
file_pathcolumn, which is
what the format-less download endpoint and OPDS serve, now prefers the path
matching the book's media type instead of always taking the ebook. And the
book list'smediaType=bothfilter now works: theebookandaudiobook
filters deliberately include dual-format books, so neither of them isolated
them, and the literal valuebothfell through unhandled and returned the
entire library. The Books page has a 📖🎧 Both button to match. -
Editing an unmonitored book no longer starts a download
(#1947) — the book update
endpoint fires an immediate indexer search whenever a book crosses into
wanted: a status edit, a "Delete file", or a media-type change that exposes
a format it does not have
(#1148). The only thing
guarding that was the global auto-grab kill-switch, so widening a book to
dual-format grabbed the missing format even when the book was explicitly
unmonitored — the one per-book control for "keep track of this, do not go and
get it". It now honoursmonitored. The status still changes and the book
still appears on the Wanted page; only the search is suppressed, and it runs
as normal once you monitor the book. The twelve-hour wanted scan already
honoured this, so nothing there changes. -
An author sync no longer walks the entire library once per new book
(#1888,
#1929) — before queuing a
search for each book it creates, the sync checks whether the file already
exists on disk, and that check did a full recursive walk of every library
root, per book. A sync that added 65 books walked the whole library 65 times.
On local disk the OS caches the directory tree and the cost hides; on an NFS
or SMB mount every walk is real network round trips per directory entry, and
at a few dozen seconds per walk this alone accounts for the reported
hour-long refresh. The sync now takes one snapshot of the library per
refresh: each root is walked once, on first use, and every per-book check is
answered from memory with the same matching rules as before — same root
selection per media type, same author-folder pre-filter, same title and
author comparison, in the same order. The walk also now honours cancellation,
which it previously ignored, so deleting an author mid-refresh stops the
filesystem work instead of letting it run to completion. One-off checks
(adding a single book, series add, recommendations) keep their per-call walk
and see the library exactly as it is at that moment; only files copied in by
hand while a refresh is mid-flight are invisible to that refresh's snapshot,
and the next refresh sees them. -
A rate-limited indexer is left alone until it says to come back
(#1934) — when an indexer
answers a search with a Newznab 500 (Request limit reached. Retry in 485 minutes.) Bindery used to record nothing, so the next search and every search
for the following eight hours sent it another request it had already refused.
The rate-limit classification existed but was consulted only inside a single
search, to stop the query cascade falling through to lower tiers. The retry
hint is now parsed out of the indexer's own message and that indexer is
skipped until the deadline passes, across the scheduled wanted scan, on-add
and bulk searches, and interactive search alike — they share one searcher, so
a limit hit by one is respected by all of them. An indexer that gives no hint
gets an hour; a parsed hint is clamped to between a minute and a day so a
malformed or absurd value cannot bench an indexer indefinitely. Editing the
indexer clears the hold immediately, so a new API key or a different account
takes effect on the next s...
v1.30.2
A maintenance release, and most of it is the same shape: something that had
been quietly not working, in a way that looked exactly like it working.
The biggest one is a tenancy bug. On a multi-user install, every book an author
sync created was written with no owner, which per-user scoping reads as
"shared" — so one user's whole catalogue was listed for every other account.
A migration repairs the rows already written. If you run multi-user, expect
books to disappear from other people's views on upgrade; that is the fix.
The rest came out of a bug sweep. Books were being dropped from a catalogue with
nothing to show for it beyond a debug log line — one reporter lost 65 books from
a single author and only found out by going looking. Import notifications were
being rejected by Apprise before they were ever dispatched, because of a payload
key whose name collides with one Apprise reserves. A book search would stop at
the first indexer response even when that response was unrelated, and never try
the queries that would have found the book. Hardcover search results could still
file a book under its narrator. An author refresh was asking OpenLibrary for the
same URL twice per work and doing every request one at a time.
Three of these were reported by users rather than found in the code, and one was
fixed by an outside contributor.
Added
- Sortable column headers on the Authors page
(#1349) — Name, Books,
Rating and Monitored are now clickable, ascending on the first click and
descending on a second, matching the Books page. This completes #1349, whose
Books half shipped in v1.28.0 while the Authors half never did. Sort keys are
whitelisted server-side and every new sort carries a name tiebreaker, so ties
cannot shuffle rows between pages of a paginated list. - Optional broad indexer categories
(#1571) — an indexer can
now opt in to searching the Newznab Books (7000) or Audio (3000) parent
category alongside its configured subcategories, which recovers releases from
trackers that file things loosely instead of under a specific child. The
parent is only ever added for a media type the indexer already carries
subcategories under, so a books-only indexer never gets an audio query, and
indexers with non-standard taxonomies (MyAnonaMouse-style100xxxIDs) are
left alone. Off by default — broad categories also return comics, magazines
and music. Set it when adding or editing an indexer; Prowlarr syncs preserve
the choice. - An author refresh now says which books it skipped, and why
(#1889) — the catalogue
sync already counted the works it dropped, but the counts only ever reached a
Debug log line per book plus one Info summary, so an author whose catalogue
had been filtered down to a handful looked exactly like an author who only
wrote a handful. One reporter lost 65 books from a single author to the
allowed-languages filter and found out only by going looking in the logs,
which a rootless container does not hand them. The author detail response now
carries alastSyncsummary — works returned, books added, and how many each
filter dropped — and the author page shows a note above the book list naming
the language set that was applied, whether the profile also rejects works with
no reported language, and a few of the dropped titles. The run's summary log
line moves fromINFOtoWARNwhen anything was skipped, so it also shows
up in Settings → Logs at the default level. Nothing about the filtering
changed: a metadata profile set to reject unknown languages still rejects
them, it just no longer does it silently. The summary is kept in memory, so it
reports syncs this process has run rather than surviving a restart.
Fixed
- Books created by an author sync now belong to the user who added the author
(#1872) —CreateForUser
wroteowner_user_idto theauthorsrow but never set it on the struct it
returned, so the catalogue sync stamped owner0onto every book it created.
A0owner is stored as NULL, which per-user scoping reads as "shared", so on
a multi-user install one user's whole catalogue was visible to every other
account. The repo now reflects the persisted owner back onto the author, and
the sync re-reads the author row before its insert loop so a stale snapshot
can no longer carry the wrong owner into new books. Migration
074_backfill_book_owner_from_author.sqlrepairs the rows already written: a
NULL-owned book under an owned author inherits that author's owner. Books
under a NULL-owned author are left alone, so deliberately shared content and
pre-multi-user libraries are untouched. On a multi-user install this will
remove books from other users' views — that is the fix working. The issue was
reported as the allowed-languages filter dropping books; the language filter
was not involved. - Import and upgrade webhooks reach Apprise again
(#1886, thanks @nathang21)
—bookImportedandupgradepayloads carried the media format under a
formatkey, but Apprise's REST API reservesformatfor the body markup
and accepts onlytext,html, ormarkdown. It rejectedebookand
audiobookwith HTTP 400 before dispatching anything, so an Apprise relay
delivered every grab, failure, and health notification — none of which carry
aformat— and silently dropped every successful import. The reserved key
is now omitted for Apprise targets only, identified by a/notifypath
segment in the webhook URL. No other consumer is affected: ntfy, Home
Assistant and Discord-proxy relays still receiveformatexactly as before,
so existing templates keep working. Every payload also carries the same value
asmediaFormat, which is never stripped, so an Apprise template has a key to
read and anyone else can migrate at their own pace. The report diagnosed this
as an emptybody; the body was in fact populated, and the reserved key was
the real reason for the 400. - Author refresh no longer spends every metadata round trip in sequence
(#1888) — a refresh that
added 65 books for one author took close to an hour. The catalogue sync loop
was not the problem: the cost was in the three per-work enrichment phases that
run before it, each a strictly serial walk of the whole work list. A 65-work
author paid 195 upstream round trips one after another before the first book
row was written, so any slow or timing-out provider multiplied straight into
wall clock. Two of those phases were also asking OpenLibrary for the same
URL twice: the work-language sampler (#891) and the work-cover sampler
(#1748) both fetch/works/{id}/editions.json?limit=5and each kept its own
cache. They now share one sample, which halves OpenLibrary requests for the
pass — measured at 130 → 65 requests for a 65-work author — and the sampling
and cover-enrichment passes run four works at a time instead of one, matching
the pace already used elsewhere for provider fan-out. On a 65-work author with
a 20 ms provider the sampling pass drops from 1.31 s to 0.35 s; against a real
provider, where a round trip is seconds rather than milliseconds, the saving
scales with it. Per-book Hardcover edition hydration inside the sync loop is
still serial and is tracked separately, and the original report has not yet
been confirmed fixed. - A junk indexer response no longer ends a book search early
(#1891) — an indexer search
runs a cascade of increasingly specific queries and stops at the first one
that works, but only the structuredt=bookquery checked that what came
back was actually about the book. The freeform tiers stopped on any response
at all, so an indexer answering "author surname + title" with unrelated
releases ended the cascade there, the relevance filter then discarded every
one of them, and the search finished with nothing — never having tried the
queries that would have found the book. Broad parent categories (#1571) make
that response much more likely, so an indexer opted in to them could return
fewer results than a narrow category list. Every tier now has to return
something plausibly on-target before it stops the cascade, and if none of
them do, the earliest tier's results are still what comes back. The extra
queries this can cost are capped at one per cascade: once any tier has
answered, the broadest query in the ladder (title with no author) is skipped,
since two more specific tiers have already failed and its results would lose
to the earlier ones anyway. - Hardcover search results no longer file a book under its narrator
(#1892) — #1733 added a
contribution-role filter so an audiobook's narrator stops being treated as its
author, but it covered only the GraphQL book queries. The Typesense search
documents carry the samecontributionfield and it was never decoded, so
every search-sourced credit arrived with an empty role, which the filter reads
as "this is the author", and the first credit won. Hardcover lists the narrator
first on plenty of audiobook-bearing works, so anything resolved through search
rather than through a book query kept the pre-#1733 behaviour. The field name
was confirmed against the live API rather than guessed — a wrong guess would
have decoded to empty and silently preserved the bug while looking fixed. - Calibre rollback previews show edition names
(#1896, thanks
@floze-the-genius) — cre...
v1.30.1
The onboarding half of this release is about the same blind spot: until now the
only evidence that first-run setup had worked was a download turning up hours
later, so a mis-wired install looked exactly like a working one. There is now a
progress checklist that ticks through indexer → download client → author → grab
→ import, warnings on the three ways a first run fails silently, and an update
badge so you find out a release exists without going looking.
The fixes are mostly a search-and-metadata sweep, and several of them are the
same shape: a feature that was fully built except for the one call that would
have made it work. The ISBN ranking bonus could never fire because nothing put
an ISBN in the search criteria. A Hardcover import list's quality profile was
saved, displayed, and never read. Reorganize's copy fallback would walk into the
directory it was creating and fill the disk. And the Hardcover client never
asked which role a credit was for, so audiobooks filed themselves under their
narrator and put themselves back there on every refresh.
Added
- Setup progress checklist on the Authors page — indexer → download client → author → grab → import, ticking off as each happens and disappearing for good once a book has imported. This is the "your setup works" confirmation the app never had: until now the only evidence that first-run setup succeeded was a download showing up hours later, so a mis-wired install looked identical to a working one. Backed by a new
GET /api/v1/system/setup-state; it replaces the getting-started card on that page (the checklist says everything the card did, plus what comes next). - In-app update badge — the header version link (and Settings → About) now turns into an amber
v1.28.0 → v1.30.0badge when a newer release exists, linking to the release. The telemetry ping already carried the latest published version on every response; the app now actually uses it instead of discarding it. Installs with telemetry disabled see no badge (the app has no other way to learn a release exists), and dev/sha builds never compare. Telemetry fleet data shows why this matters: docker installs are 81% current within a week of a release, while binary installs are 35% — the gap is discovery, not willingness. - "Updated to vX — see what's new" toast after an upgrade, closing the loop the update badge opens: the badge tells you an update exists, this confirms it landed and links to the release notes. Shows once per version, never on a first-ever load (a fresh install has nothing to catch up on), and never for dev/sha builds.
- Setup-funnel telemetry (anonymous, opt-out with the rest of telemetry) — the daily ping now includes whole-day offsets from install to first indexer, first download client, first author, first grab, and first import (e.g.
setup_indexer_day: 0= configured the same day). Integers only, never timestamps. Fleet data showed installs that reach "indexer + download client" retain 66% at 7 days vs 16% for those that don't — these fields make the stall points in that funnel visible so onboarding changes can be measured. Documented at getbindery.dev/telemetry-fields.
Fixed
- German titles with umlauts find their releases (#1610) — indexer queries were sent with literal ä/ö/ü/ß while Usenet release names use the ASCII convention (Phönix vs. Phoenix), so every umlaut-containing title returned (near-)zero results and never auto-grabbed. Book-search queries now transliterate German umlauts (ä→ae, ö→oe, ü→ue, ß→ss) before being sent; other Latin diacritics (é, ñ, ç) and non-Latin scripts are left unchanged, and free-text searches are untouched. When the transliterated query finds nothing at all, the search retries once with the original umlaut spelling to catch the rare releases that keep it.
- Hardcover no longer files books under their narrator or translator (#1733) — the client never asked Hardcover which role a credit was for, so it took whichever contribution came back first and matched author lookups on name alone. Books landed under an audiobook narrator (Will Wight's Cradle volumes showing Travis Baldree as the author), and because the per-author refresh matched every contribution role by name, correcting a book by hand only held until the next "Refresh All Metadata" put it back. Every book query now requests the contribution role, the author lookup only matches author-role credits, and metadata resolves to the actual author. Books whose only credit is an editor or illustrator keep that credit rather than losing an author entirely.
- Reorganize no longer fills the disk when a book's new location sits inside its old one (#1809) — a layout change that computes a destination nested under the source (a series folder named after the book, so
/library/Author/Titlemoves to/library/Author/Title/Title, or a flat author folder that becomes/library/Author/Book/) cannot be renamed by the kernel, so it fell through to the copy-based move, which then read the source while the destination grew inside it: the copy descended into its own output and nested directories forever until the drive was full and the container had to be killed. Directory moves, copies and hardlink placements now check containment up front and refuse the move with an error naming both paths, and the reorganize preview flags it before anything is applied. Source and destination are resolved through symlinks and compared on path component boundaries, so a genuine sibling likeBook Twonext toBookstill moves normally. - "Search all wanted" no longer floods an indexer with duplicate and empty-term queries (#1814) — a 26-book author sent roughly 294 searches to one indexer over 15 minutes, and every search after the first minute failed with
context deadline exceededso nothing was grabbed. Three things multiplied: a book whose title normalised to nothing (a blank title, or a row whose title is only an edition qualifier like(Unabridged)) still ran the full four-tier query cascade and putq=on the wire with nothing after it; two catalogue rows for the same work produced byte-identical queries that were both sent, and a sweep overlapping an earlier one repeated every query that sweep had already made; and the bulk fan-out ran at twice the concurrency of every other search fan-out in Bindery. Unsearchable titles are now skipped outright, identical queries to one indexer are collapsed for 90 seconds, and the bulk fan-out runs at the same bound as the per-author auto-search. A repeat or overlapping sweep is now close to free. - Releases that name a book's ISBN now actually win the search (#1724) — the ranker has always carried a large exact-match bonus for a release whose title contains the book's ISBN, but nothing ever put an ISBN into the search criteria, so the bonus could not fire on a single search Bindery has ever run. Both search paths (interactive search and the scheduler's auto-grab) built their criteria with title, author, year and ASIN and silently left the ISBN empty, which is why an ISBN-tagged release ranked no better than an untagged one and auto-grab could pick the wrong edition when the right one was sitting there labelled. Both paths now read the book's editions and populate the criteria, converting an edition recorded only as an ISBN-10 into its ISBN-13 form so it matches — a release name can only ever carry the ISBN-13.
- One-word book titles no longer grab a longer book that merely starts with that word (#1731) — a book titled Treasure auto-grabbed and silently imported
Clive Cussler - Dirk Pitt Universe Bk 29 - The Treasure of Khan, a different book by the same author, which landed under the right book record with no error. The single-keyword match path only asked for the word at a word boundary plus the author somewhere in the release, and the author corroboration added for embedded titles cannot separate two books by the same author. The matcher now looks to the right of the matched word as well as the left, and rejects a release where the word runs on through a connective (of,and,in,to, …) into a further title word. Trailing years, formats, bitrate and edition markers, series labels, language tags and the author's own name are all still accepted, as are subtitles opening with an article (Treasure - A Dirk Pitt Novel), so correctly named releases keep matching. - Series volumes sharing a base title no longer collapse into one book (#1785) — the canonical dedup key strips a
": subtitle"tail, so a series titledSeries: Volume(e.g. Tao Wong's A Thousand Li, the Gears of War novels, omnibus collections) mapped every volume onto a single key. On an Audiobookshelf import that merged the extra volumes (a 902-item library created only ~824 books, the rest linked or queued for review); on a manual add by provider id it left the requested id bound to nothing, so the request failed forever with "book not found after author sync — try again shortly". The dedup match is now series aware: a candidate that shares a series but sits at a different sequence number is treated as a distinct work, so each volume gets its own row. Same-sequence editions (an audiobook that drops the subtitle) still merge as before. - Hardcover list sync no longer stalls on large lists (#1694) — syncing a list issued one fully-paginated GraphQL edition query per newly imported book, so a first sync of a big shelf ran out of the request's time budget after a handful of books and reported success anyway. The audiobook ASIN, language, and media type those queries were after now arrive inline on the list response itself (via the default-edition relations — the
bookstype has nolanguagefield of its own). Audnex audiobook enrichment still runs for books whose ASIN arrived inline, without any edition fetch. Measured on a real 1,660-book shelf against v1.30.0: the scheduled (deadline-free) sync now completes the full list; the manual "Sync n...
v1.30.0
Mostly a UI release for the Author and Book detail pages, which had drifted
into looking unfinished. Two of those fixes are the same bug wearing different
clothes: a Tailwind class that compiles to nothing, and a Tailwind class that
never compiles at all. Both had been shipping silently — the class name looks
right in the DOM and the build succeeds, so nothing in the toolchain noticed the
File card had collapsed to one column or that the author page had no width limit
at all. Two lint rules now fail CI on either pattern. The rest of the UI work is
structure: one shared width across both pages, overflow menus instead of
eight-button rows, selects instead of ten filter chips, and a cover placeholder
that no longer reads as a broken image.
Riding along: backups can carry a label instead of a bare timestamp, Audiobookshelf
imports stop re-querying the same author once per book, and Hardcover list-sync
authors finally get the default metadata profile every other path assigns.
Added
- Label a backup when you create it (#1790) — the Backup panel takes an
optional label, so a snapshot is saved asbindery_<timestamp>_<label>.db
(e.g.bindery_20260726_181731_pre-import.db) rather than a bare timestamp
you have to rename afterwards to recognise. Labelled backups restore and
delete correctly from the UI, which previously accepted only the
bare-timestamp filename; a backup renamed by hand to something outside the
bindery_*shape still lists but cannot be restored or deleted from the UI.
The label is sanitised before it reaches the filename — onlyA-Za-z0-9_-
survive, everything else collapses to-, capped at 40 characters — so a
label that reduces to nothing (an all-CJK one, for instance) is dropped and
the snapshot keeps its plain timestamp name.POST /api/v1/backupaccepts the
optional{"label": "..."}body; sending none behaves exactly as before. - Series name and position on the book detail page (#1795) —
series_books
has been populated since v0.7.0 and this page never surfaced it. A book that
belongs to a series now shows it in the meta row (Discworld #3), once per
series for books that belong to several. There is no book→series endpoint, so
this reusesGET /author/{id}/series; the lookup runs after the book loads,
never blocks rendering, and simply omits the row if it fails. DownloadingandSkippedfilters on the author page (#1795) — both
values have been in the status filter's type since it was written, but the
chip row only ever offered All / Wanted / Downloaded / Imported, so there was
no way to see books in either state. The status control now offers every value
it supports.- Books without cover art get a real placeholder (#1795) — previously a flat
grey box with small centred text, which read as a failed image load; on a
library where half the covers are missing, that is most of the page. The
placeholder now sets the title large over a ground colour derived from the
book's id, so it is stable per book and consistent everywhere that book's
cover is drawn. Every colour carries white text at 4.5:1 or better and sits at least 3:1 from
both page backgrounds, enforced by tests.
Changed
- Both detail pages now use one container width (#1795) — the author page
was effectively7xland the book page4xl, so following a link from an
author to one of their books collapsed the content by 384px and shunted it
left. Both are now7xl, matching every other page; descriptions are held to
a readable measure individually rather than by narrowing the whole page. - The author page's action row is five controls instead of eight (#1795) —
it wrapped, which pushed Delete onto a line of its own and gave the most
destructive action the most prominence by accident. Monitored, a primary
"Search N wanted" that carries its count in the label, Refresh and Edit stay
on the row; Rename files, Merge, Link metadata and Delete move into a More
menu (keyboard-navigable, Escape and click-outside to close). - Author page filters are three selects on one line (#1795) — replacing three
labelled chip groups totalling ten buttons, plus a "Select all" that wrapped to
a second row and read as though it belonged to the Published group. - "Show excluded" is now an option in the status filter, not a separate
checkbox (#1795) — note the behaviour change: it used to add excluded
books to whatever you were looking at, and now narrows to only them,
consistent with every other option in that list. It is also remembered between
visits, which the checkbox never was. - The author page's stats are a fixed four-cell strip (#1795) — the old
run-on line dropped the audiobook count entirely when it was zero, so the row
changed shape between authors and no figure ever appeared in the same place
twice. Books / In library / Wanted / Audiobooks are now always all four. - Book detail: Edit moved to the header, and the File card's actions are
ranked (#1795) — Edit changes metadata, not the file, and was the one action
in that row with nothing to do with bytes on disk. Download, Re-bind and Fix
match stay visible; Exclude and Rename files move behind More. "Delete
file" drops from solid red to an outlined destructive style: it is reversible
by re-downloading, and it was louder than "Delete book + files", which is not.
Solid red now appears exactly once per page, on the action with no undo. - Long book descriptions clamp with show more/less (#1795), matching the
author page, instead of running the full height of the page.
Fixed
- Audiobookshelf imports no longer re-query the same author once per book
(#1788) — the importer looked each book's author up against the metadata
providers with no caching, so every book on the same shelf re-issued an
identical provider search. When that search was slow or unreachable —
OpenLibrary author search timing out on romanised-CJK pen names, Hardcover
returning 401 — each repeat paid the full per-request timeout again, dragging
a single author's shelf out to minutes. The lookup is now memoised for the
duration of one import run. Note the trade: a provider that degrades
mid-import stays degraded for that author until the run ends, where
previously each book got a fresh attempt. - Hardcover list-sync authors now get the default metadata profile (#1736,
#1783) — they were created with no profile assigned instead of the default
"Standard", and existing rows are backfilled by migration. No behaviour
changed as a result, because every reader already fell back to the default;
what it fixes is the profile shown in the UI and three separate fallbacks
that had to stay in sync. Five other author-creation paths still insert no
profile, so the migration is a one-shot cleanup rather than a permanent fix —
tracked in #1803. - The book detail File card had collapsed to a single column (#1791) — its
label/value grid separated the two tracks with a comma, which Tailwind passes
through verbatim into an invalidgrid-template-columnsdeclaration that every
browser then drops. The rule was generated, so grepping the compiled CSS for
the class found it and it looked fine; only the computed style showed a single
track. A test now asserts the card resolves to two tracks, and a lint rule
fails CI on a top-level comma in any arbitrary value. - The author detail page was rendering with no width constraint at all
(#1791) — itsmax-w-5xlsat directly against a${…}interpolation, and
Tailwind v4 scans source text rather than runtime values, so it extracted the
tokenmax-w-5xl${selected.sizeand never emitted.max-w-5xl. Neighbouring
widths were present in the CSS, which is what made it look like a framework
bug rather than a source one. A second lint rule now fails CI on any class
glued to an interpolation, and a sweep of the tree found no other instance. - Clicking a table row on the author page did a full page reload (#1795) —
the row navigated viawindow.location.hrefwhile the link inside that same
row routed client-side, so one row had two different behaviours depending on
where you clicked. - Grid cards no longer end at ragged heights (#1795) — the text block is a
fixed height, and the published year no longer appears and disappears between
cards (the formatter already returns an em dash for a missing date, so the
surrounding conditional only ever removed the row).
Docs
BINDERY_TRUSTED_PROXYgoverns the forwarded scheme and host, not just
proxy auth (#1787) — the deployment reference now says so. Requests from a
peer outside this list have everyX-Forwarded-*header stripped, so behind
a TLS-terminating reverse proxy the OPDS feed links come outhttp://, and
BINDERY_COOKIE_SECURE=autoand the OIDCredirect_uriboth see the wrong
scheme, until the proxy's IP or CIDR is trusted here. Set it even if you are
not using proxy auth.
Security
- Bumped js-yaml to 4.3.1 (#1793, GHSA-5p4m-2wfm-xmqj) — resolves a
high-severity quadratic-CPU advisory in!!omapresolution, where the
CVE-2026-59870 fix was never backported to the affected 4.x range. Build
tooling only: js-yaml reaches the tree as a transitive devDependency of ESLint
and never enters the browser bundle, so no running instance was exposed.
v1.29.1
A patch release out of a codebase audit. Two of these are credential leaks that
only mattered once Bindery went multi-user: an indexer API key riding along in
every search and queue response, and profiles you could read or delete across
users because their owner was never stamped. The rest are the same shape as
v1.29.0 — settings that saved and then did nothing: a Calibre "Library import"
toggle no code read, naming templates that only applied after a restart, and
two clients quietly ignoring the outbound proxy. Plus one dead endpoint removed.
Security
- Metadata and quality profiles created via the API are now scoped to their creator —
Createnever wroteowner_user_id, so withBINDERY_ENFORCE_TENANCYon, every API-created profile was owner-less and the per-user access check (CheckOwnership) treated it as shared, letting any authenticated user read, edit, or delete another user's profile. Both profile Create paths now stamp the caller's user id (CreateForUser), matching authors/books. Existing owner-less rows stay shared, as before. - Indexer API keys no longer leak to non-admin users in search and queue responses — interactive indexer search signs the instance's indexer/Prowlarr apikey into each result's
nzbUrl, and that download URL was returned verbatim to any authenticated user (search results and the queue list). The apikey is now stripped from every client-facing response and re-attached server-side at grab time from the release's indexer id, so grabbing still works while the shared credential stays off the wire.
Fixed
- Book covers now backfill on Refresh Metadata, and edition covers are consulted (#1748) — a book row that imported without a cover used to stay blank forever: the author refresh path updated only ratings and genres on existing rows, never
image_url, so clicking Refresh Metadata could not fill a missing cover even once one was available upstream. The refresh now fills an empty cover (never overwriting one you already have). Separately, OpenLibrary attaches covers to editions far more consistently than to works, and Bindery previously read only the work-level cover; cover-less works are now sampled against their editions (bounded and memoized, same as language sampling) so a work whose cover lives only on an edition still gets one on add and on refresh. - The Calibre "Library import" toggle now actually gates library imports (opt-in) — the setting was UI-only: no backend code read it, so startup imports, the 24h scheduled sync, and the manual import ran off
calibre.library_pathregardless of the toggle. A user who saw the switch "off" was still being imported on every boot. All three import paths now honorcalibre.library_import_enabled. Existing installs that already have a library path configured are backfilled to enabled by a migration, so no working import is disabled; a newly-configured library imports only after you turn the toggle on. - Hardcover list sync and Prowlarr sync now honor
BINDERY_OUTBOUND_PROXY—hardcover.NewAuthenticated(used by the import-list syncer and import-list browse) and the Prowlarr client were built without the proxy transport, so they dialedhardcover.app/ the Prowlarr host directly while every sibling code path was proxied. On a locked-down egress they failed outright; on a VPN-only setup they leaked traffic outside the configured proxy. Both now use the shared proxy transport like the other clients. - Ebook/audiobook naming templates now take effect without a restart — the destination templates (
naming.bookTemplate,naming_template_audiobook) were read once at boot and baked into the renamer, so saving a new template in Settings did nothing until Bindery restarted — and Reorganize actively applied the stale boot-time template. Both templates are now re-read from settings per import and per reorganize, matching how the per-track audiobook template already worked. - Google Books API key and primary metadata provider now show a "restart required" hint — both are read once at boot, so a change only takes effect after a Bindery restart. The settings fields now say so (matching the existing wanted-search-interval note), instead of silently appearing to save with no effect.
Removed
- Dead
POST /book/{id}/mapendpoint — an undocumented metadata-map handler with no caller (the Fix Match UI usesrebind, and ABS review uses its own resolve endpoint). Removing it drops maintained authenticated surface that duplicated rebind's logic; the shared helpers it used remain in place for the audiobook ASIN-map path.