Skip to content

Releases: vavallee/bindery

v1.32.2

Choose a tag to compare

@github-actions github-actions released this 25 Aug 05:12

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.db but 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...

Read more

v1.32.1

Choose a tag to compare

@github-actions github-actions released this 20 Aug 22:46
680f83c

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: with synchronous=NORMAL an 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 check now runs govulncheck (#2140), pinned to the same revision CI installs. CONTRIBUTING.md described 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 ran make check before opening a pull request had not actually run the scan CI would fail them on.

  • Contributor documentation corrected and extended. ARCHITECTURE.md claimed SQLite reads run concurrently, which the single connection pool has never allowed. CONTRIBUTING.md gains 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_groups now 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-less host:port as 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.txt and ui-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-For was 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/stats and /api/backup bearer check used a plain string comparison, whose timing leaks how much of a guess was correct. /api/backup returns 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...

Read more

v1.32.0

Choose a tag to compare

@github-actions github-actions released this 19 Aug 06:42
3809a97

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/bulk rejected mediaType: "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. both is 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) — AuthorSyncSummary carries 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 ": ", so Star Wars: A New Hope and Star Wars: The Empire Strikes Back were 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, minPages and skipMissingIsbn were 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 token and then 429 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 logs hardcover enrichment idle: no api token configured in 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.excluded column 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=audiobook delete landed on a directory. That branch was an unconditional os.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's book_files row 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_files ownership 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_mode and 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_DIR differs 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 generic no_title_match reason, 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...

Read more

v1.31.0

Choose a tag to compare

@github-actions github-actions released this 14 Aug 23:32
18ac850

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

Choose a tag to compare

@github-actions github-actions released this 13 Aug 18:31

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 with migration 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-check and bindery db-repair
    (#1972) — offline database
    integrity tooling that runs without applying migrations, so it works on an
    instance that cannot start. db-check lists every row whose foreign key points
    at a missing parent and changes nothing; db-repair --yes replays the delete
    rule the schema declares (ON DELETE CASCADE rows removed, ON DELETE SET NULL references cleared, anything else skipped and reported) and prints what
    it did. --yes is required; without it the command refuses and names the file
    to back up. Also reachable as BINDERY_DB_FK_CHECK=report|repair for 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

Choose a tag to compare

@github-actions github-actions released this 12 Aug 19:17
ec43ca5

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_files records 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
    audiobook that also held an epub showed a 🎧 Audiobook badge (from the media
    type) next to the epub's path (from the legacy file_path column, 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-less DELETE, 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 the book_files migration 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 to both on 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_path column, 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's mediaType=both filter now works: the ebook and audiobook
    filters deliberately include dual-format books, so neither of them isolated
    them, and the literal value both fell 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 honours monitored. 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...

Read more

v1.30.2

Choose a tag to compare

@github-actions github-actions released this 11 Aug 20:48
10c0def

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-style 100xxx IDs) 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 a lastSync summary — 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 from INFO to WARN when 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
    wrote owner_user_id to the authors row but never set it on the struct it
    returned, so the catalogue sync stamped owner 0 onto every book it created.
    A 0 owner 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.sql repairs 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)
    bookImported and upgrade payloads carried the media format under a
    format key, but Apprise's REST API reserves format for the body markup
    and accepts only text, html, or markdown. It rejected ebook and
    audiobook with HTTP 400 before dispatching anything, so an Apprise relay
    delivered every grab, failure, and health notification — none of which carry
    a format — and silently dropped every successful import. The reserved key
    is now omitted for Apprise targets only, identified by a /notify path
    segment in the webhook URL. No other consumer is affected: ntfy, Home
    Assistant and Discord-proxy relays still receive format exactly as before,
    so existing templates keep working. Every payload also carries the same value
    as mediaFormat, 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 empty body; 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=5 and 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 structured t=book query 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 same contribution field 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...
Read more

v1.30.1

Choose a tag to compare

@github-actions github-actions released this 11 Aug 02:57
df92a02

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.0 badge 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/Title moves 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 like Book Two next to Book still 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 exceeded so 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 put q= 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 titled Series: 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 books type has no language field 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...
Read more

v1.30.0

Choose a tag to compare

@github-actions github-actions released this 07 Aug 15:28

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 as bindery_<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 — only A-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/backup accepts 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 reuses GET /author/{id}/series; the lookup runs after the book loads,
    never blocks rendering, and simply omits the row if it fails.
  • Downloading and Skipped filters 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 effectively 7xl and the book page 4xl, so following a link from an
    author to one of their books collapsed the content by 384px and shunted it
    left. Both are now 7xl, 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 invalid grid-template-columns declaration 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) — its max-w-5xl sat directly against a ${…} interpolation, and
    Tailwind v4 scans source text rather than runtime values, so it extracted the
    token max-w-5xl${selected.size and 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 via window.location.href while 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_PROXY governs the forwarded scheme and host, not just
    proxy auth
    (#1787) — the deployment reference now says so. Requests from a
    peer outside this list have every X-Forwarded-* header stripped, so behind
    a TLS-terminating reverse proxy the OPDS feed links come out http://, and
    BINDERY_COOKIE_SECURE=auto and the OIDC redirect_uri both 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 !!omap resolution, 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

Choose a tag to compare

@github-actions github-actions released this 04 Aug 20:05
8b30fe8

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 creatorCreate never wrote owner_user_id, so with BINDERY_ENFORCE_TENANCY on, 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_path regardless of the toggle. A user who saw the switch "off" was still being imported on every boot. All three import paths now honor calibre.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_PROXYhardcover.NewAuthenticated (used by the import-list syncer and import-list browse) and the Prowlarr client were built without the proxy transport, so they dialed hardcover.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}/map endpoint — an undocumented metadata-map handler with no caller (the Fix Match UI uses rebind, 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.