github vavallee/bindery v1.33.4

2 hours ago

A code audit, and the eight things it found that were wrong without anyone reporting them.

Nothing in this release came from a bug report. It is the output of a scan of the
whole tree, so the entries share a shape: each one is something Bindery did
quietly and confidently, on a path where nobody was watching. Three of them are
reachable by any account you have ever created, including a read-only reader
account. One of them destroys the database you asked it to protect.

Added

  • Safe author catalogue reconciliation (#2208). Preview, select, and explicitly remove stale metadata-only Wanted rows after a provider or metadata-profile change, while imported and file-bearing books are protected from the sweep.

Fixed

  • Restoring a backup no longer reverts or corrupts the database it restored (#2337). Restore copied the backup file over the live database while Bindery still had it open, and left the write-ahead log beside it untouched. SQLite validates those log frames against the log's own header rather than the database file, so everything written since the backup was replayed straight back over the restored data at the next checkpoint, and anything written between the restore and the restart went into the same log. The visible result was a restore that appeared to succeed and then quietly undid itself. Restore now validates the backup, stages it, and returns; the swap happens at the next start, before the database is opened, with the stale log files removed. This changes how restore behaves: the endpoint returns immediately but nothing takes effect until you restart Bindery. A staged file that turns out not to be a readable SQLite database is parked as .restore-failed and the live database is left alone. The staged file has no expiry, so if you change your mind before restarting, delete bindery.db.restore-pending.
  • A book that wants both formats no longer stops searching for the second one (#2365). The wanted sweep skipped any book with a download in flight, counted per book rather than per format. Two states never end on their own: a hand-off to an external import tool, and a format held back waiting for its pair. A book parked in either therefore left its other format unsearched for as long as the download row existed, with nothing said. Under pair gating this was a deadlock, because the sibling the hold waits for could only ever arrive through the search the hold had just disabled. The sweep now tracks in-flight downloads per format. A book whose format cannot be determined from the release is still skipped whole, and a book monitored for a single format is skipped whenever it has any live download, because guessing there could only free the slot that is already downloading. After upgrading, books that have sat idle may start downloading their missing format; the searches are paced at the same rate as any other sweep.
  • The Hardcover series view no longer collapses a numbered series onto one volume (#2343). Volume numbers were compared with a fuzzy title score that reads "Vol. 1" as a substring of "Vol. 13", so the two scored a perfect match. If your library had no recorded position for a book, which is normal after an Audiobookshelf or Calibre import, owning volume 13 showed volume 1 as present under volume 13's title, and left volume 13 in the missing list. Only numbers carrying an earlier number as a digit prefix collided, so 10 through 19 against 1, and 21 against 2. Volume numbers now veto the similarity score, the same way adding a book already did. A catalog entry can also no longer be claimed by two of your books at once. The view is rebuilt on every load, so it corrects itself as soon as you open it.
  • An Audiobookshelf item with no series sequence no longer binds to the wrong volume (#2347). The same fuzzy comparison, on the import side. When the item carried no sequence number, every volume in the series was scored against its title at a threshold below the range sibling volumes actually score in, and the item was linked to whichever scored highest, which the substring rule usually made the longest number. Items with a sequence were never affected. Unlike the series view, this one does not heal on its own: volumes already linked to the wrong Hardcover entry stay linked to it after upgrading.
  • "Retry all failed" now retries the rows it says it will (#2336). The button filtered on Import Failed alone while the retry endpoint behind it has always accepted Import Blocked too, so a queue full of blocked rows offered a button that did nothing. The queue's status labels, colours, and the failed and matchable sets are now derived from one table instead of four hand-maintained lists that had drifted apart. Note that the failed count does not drop when you press it: a retried row returns to Import Failed until the scanner runs again, and the signal to watch is the error text clearing.
  • The queue no longer shows a raw internal name for two of its states (#2339). A download handed to an external import tool, or held waiting for its paired format, rendered as importExternal or importHeld in a grey chip with no explanation, which is exactly when a user most needs to know why a row is not moving. Both now have a label and a line saying what they are waiting for.
  • Six reads of a third-party response are no longer unbounded (#2357). Replies from Prowlarr, qBittorrent, NZBGet, and an indexer's capability document were read into memory whole, with no ceiling, on a schedule and unattended. A misbehaving or hostile service could exhaust memory. The caps are set well above what a large install produces: the qBittorrent limit holds around 28,000 torrents, against 5,000 for the largest install anyone has reported. Reaching one now reports what happened instead of failing later as a truncated document.

Security

  • The OIDC discovery probe is now admin-only (#2348). POST /auth/oidc/test-discovery was registered outside the admin group, so any authenticated account could point it at an address on your network and read the outcome. The four possible answers are distinguishable from each other, which turns the endpoint into a way to map internal hosts and ports from inside the Bindery container. The handler always believed it was admin-only; its own comment said so. Reachable by any account, including a reader account created only for OPDS.
  • The Google Books API key is no longer readable by every account (#2351). The rule that keeps secrets out of the settings list matches names ending in .api_key or .api_token, and both spellings of this key match neither, so it was returned in full to anyone signed in. It is now write-only, like the Hardcover token. The field on Settings → API Keys now loads empty. Your key is still stored and still in use. Type a new one only if you want to replace it; the Save button beside it stays disabled while the box is empty, so an accidental save cannot wipe what is stored.
  • A bulk blocklist delete can no longer be used to exhaust memory (#2354). The request body size limit skipped DELETE entirely, on the assumption that no handler read a body from one. DELETE /blocklist/bulk does, and it is open to any authenticated account, so a large enough body could pin the process while Go grew the buffer to hold it. DELETE bodies are now capped like every other method, and an oversized one is refused with a clear status instead of being decoded.

Don't miss a new bindery release

NewReleases is sending notifications on new releases.