# Changelog All notable changes to Bindery are documented here. Format loosely follows [Keep a Changelog](https://keepachangelog.com) and versions follow [Semantic Versioning](https://semver.org). ## [Unreleased] ## [v1.26.0] — 2026-07-14 A correctness release. Two independent reports pinned real bugs in the parts of Bindery that decide *what to grab* and *what to do with it afterwards*: the relevance filter could grab a completely different book whose title merely contained yours, and usenet imports were quietly leaking every completed download folder to disk forever. Both are fixed. Around them, this release is mostly about Bindery telling you the truth when something is wrong — storage health, VPN-blocked connection tests, and Grimmory's connection probe all stop hiding behind generic errors — plus a search-first Add Author flow and a Calibre path-remap escape hatch for mismatched container mounts. ### Added - **Calibre push path remap** (#1346) — Bindery hands the Bindery Bridge plugin the exact path it stores each book at, and the plugin opens that path on *its* side of the container boundary; when the two containers mount the library at different points (the recurring Unraid case), every push failed with "No such file or directory". A new **Settings → Calibre → Push path remap** field (plugin mode) translates Bindery's library prefix to the Calibre container's before the push, using the same `from:to[,from:to]` grammar as `BINDERY_DOWNLOAD_PATH_REMAP` — e.g. `/books:/mnt/user/media/books`. Malformed pairs are rejected at save time; empty means no translation, and aligning the mounts remains the preferred zero-config setup. ### Changed - **Search-first author acquisition** (#1227, #1516) — select an author result before reviewing monitoring and download options, with consistent localized add dialogs and inline errors. Stage 1 of unifying the add and search acquisition flows; the issue stays open to gather feedback before the next stage. Community PR from @magrhino. - **Base image and dependencies refreshed** (#1527, #1528, #1529) — the distroless `static-debian12` base moves to its current digest, alongside the usual grouped Go and frontend dependency bumps. ### Fixed - **Usenet imports no longer leak completed job folders or library receipts** (#1542) — `import.mode` was applied with no protocol distinction, so usenet downloads inherited hardlink/copy behaviour whose only purpose is preserving torrent seeding. The completed job folder was left behind forever — and invisibly, since post-import cleanup removes the client's history entry but not its files (one report: 2.4 GB orphaned from three audiobook grabs). SABnzbd/NZBGet downloads now resolve `auto` and `hardlink` to `move` (explicit `copy` stays honoured for operators who want the client's retention to see finished files). Separately, directory placements copied the whole job tree verbatim, so `.nzb` receipts and `.par2` repair files landed in the library next to the media: hardlink, copy, move, and multi-disc-flatten placements now all skip download artifacts (`.nzb`/`.par2`/`.sfv`/`.srr`/`.srs`/`.diz` — `.nfo`, covers, and cue sheets are deliberately kept). Multi-disc flattening also works under move mode now (flatten via copy, then remove the source), so usenet downloads resolving to move don't lose it. Reported by cleb on Discord with both root causes correctly identified. - **Wrong-author grabs from embedded title phrases** (#1539) — the release relevance filter accepted any release whose name contained the book's title words as a contiguous phrase, with no author check on that path, so a different work embedding the requested title could be grabbed and imported ("Reborn as an Assassin's Apprentice, Vol. 1 by okiuta" matched for Robin Hobb's "Assassin's Apprentice"; reported by cleb on Discord with the root cause pinned). Phrase and in-order title hits are now only trusted on their own when *anchored* — preceded by nothing but the author, a series index ("Book 1", "Vol. 2"), numbers, or filler words. When real foreign words sit in front of the title (usually another work's longer title), the requested author must appear somewhere in the release name. Releases titled with just the book title still pass, so the fix costs no recall on the common author-less naming shapes; the narrow tradeoff is that a release naming only a series *name* (no author, no "Book N" marker) before the title is now rejected rather than risk importing the wrong book. - **Bulk searches no longer burst your indexers** (#1515) — "search all wanted" for a prolific author, filling a series, per-author auto-search, and the scheduled wanted loop now pace their indexer searches (a short gap between each) instead of firing as fast as slots free up. A 30-book author could previously flood a rate-limit-free Prowlarr into dropping requests, so nothing got grabbed; the fan-outs still run with the same concurrency caps but no longer sustain a tight query loop. - **Transmission polling on large torrent histories** (#1524) — the download poller read at most 1 MiB of Transmission's `torrent-get` reply, so an instance with a few thousand torrents (one report: ~12 MiB for 5000+) had its response silently truncated and every poll failed with "unexpected end of JSON input", blocking imports. The RPC read cap is now 64 MiB, and a reply that somehow still exceeds it returns a clear "too many torrents to poll in one request" error instead of invalid JSON. - **Grimmory "Test connection" against Grimmory v3.x** (#1485) — the connection test probed `GET /api/status`, a route current Grimmory (v3.x) no longer has. Its Spring security layer answers any unmapped `/api/**` path with a 401 Whitelabel page, which looked like an auth wall (and was mistaken for one in #1448) but was really a missing route, so the test could never pass and failed with `invalid character '<'`. The probe now hits Grimmory's public `GET /api/v1/healthcheck` endpoint for reachability and version; credential verification stays with the separate login round-trip, so "Test connection" still reports whether your username/password actually work. - **Storage health now says *why*, not just *that*** (#1427) — the "downloads and library can't hardlink" banner and the "not writable" warnings were generic, sending operators hunting through mount tables and permission bits blind. The hardlink probe now names the actual cause (different filesystems; same device ID but cross-mount EXDEV, typical of mergerfs pools, separate bind mounts, and Unraid `/mnt/user` shares; or a filesystem that refuses hardlinks, common on exFAT/NTFS/network shares) in Settings → General. And a failed writability check now reports the uid/gid the process actually runs as versus who owns the directory, with the `user: "UID:GID"` hint when they differ — the classic case being folders prepared for the stack's usual user while the container runs as the distroless default `65532` because `user:` was never set in Compose. - **VPN-killswitch timeouts are named, not just reported** (#1474) — when an ABS or Calibre-plugin "Test connection" times out against a LAN-shaped host (private IP, bare Docker hostname, `.local`/`.lan` suffix), the error now points at the usual culprit: Bindery sharing a VPN container's network (`network_mode: service:gluetun`) whose killswitch drops LAN traffic, with the `FIREWALL_OUTBOUND_SUBNETS` fix named inline. Real upstream errors (auth failures, refused connections) and public hosts keep their unmodified message. Complements the "Running Bindery behind a VPN" deployment docs. - **Go race CI no longer times out** (#1531) — split the race suite into six parallel shards so the large API and database test packages stay below their per-package deadlines. Community PR from @magrhino. ### Docs - **Shared ebook + audiobook folder layout, and what `BINDERY_DOWNLOAD_DIR` actually does** (#1426) — the Storyteller-style shared folder (an ebook and its audio files in one directory) is Bindery's *default* behaviour when `BINDERY_AUDIOBOOK_DIR` is left unset, since both naming templates share the `{Author}/{Title} ({Year})` prefix; now documented with the resulting tree. And `BINDERY_DOWNLOAD_DIR` is **not** a watch folder — per-job import paths come from the download client's API, and the env var only feeds validation, storage health, the hardlink probe, and qBittorrent save paths — so the TRaSH split-tree layout (`/data/torrents` + `/data/usenet`) works as-is. ## [v1.25.0] — 2026-07-10 A contributor-driven release. The headline is manual metadata editing with field locks — edit a book's title, genres, language, description, or release date and every refresh path keeps your values — which also unlocks clean opinionated genre taxonomies in folder paths. Around it: naming-template conditionals and zero-pad widths, a `{Lang}` token, an ebook/audiobook/both selector on Add Book, per-account Hardcover reading lists, an honest Import Mode default (community PR), multi-user tenancy fixes for owner stamping and the series views, and the next batch of audit bug fixes. ### Added - **Manual metadata editing with field locks** (#1237, #1446) — the book page gains an Edit action for title, description, genres, language, and release date. Every field you edit is locked so the nightly metadata refresh, author-works sync, and ABS/Calibre re-imports keep your values (a 🔒 marks locked fields; one click unlocks them all). Genres can also be applied in bulk: an author-level override in Edit Author and a Set genre action per series stamp + lock the same genre list across all their books, so an opinionated genre taxonomy in folder paths (`{Genre}/...`) stays clean. Re-bind and metadata re-map clear locks — an explicit identity change asks for the new record wholesale. See the new [Metadata Editing wiki page](docs/Metadata-Editing-Wiki.md). - **Conditional text and zero-pad widths in naming templates** (#1127) — literal text placed inside a brace group renders only when its token has a value (`{Title}{ - Series}` emits the dash only for series books, no more trailing separators), and numeric tokens accept a width modifier (`{SeriesNumber:2}` → `02`) so alphabetic filename sorts keep parts in order. Both are backward compatible: bare tokens, `{Genre:Unsorted}`-style defaults, and unknown-token passthrough behave exactly as before. One edge: a 1–2 digit modifier is now a width, so a numeric *default* that short is no longer supported (3+ digits, e.g. `{Year:2024}`, still works as a default). - **`{Lang}` naming token** (#1175) — naming templates can now include the book's language code (e.g. `en`), so foreign-language books can carry the language in their folder or file name. Blank when the language is unknown, collapsing cleanly like the other optional tokens. - **Format selector on Add Book** (#1397) — the Add Book modal (Authors/Home page) now has an ebook / audiobook / both selector, matching the Series page. Default keeps the previous behaviour (provider metadata, falling back to the `default.media_type` setting). Picking a format applies it to the added book even when it already existed in the library, re-evaluating wanted status so the missing format gets searched. ### Security - **Multi-user tenancy: owners are now stamped and series views scoped** (#1457, #1416) — new books inherit their author's owner on every create path (add-book, author sync, series fill, ABS/Calibre imports), and new downloads carry the grabbing user (background grabs inherit the book's owner), so per-user scoping finally has data to scope on: a non-admin's queue is no longer empty under tenancy, and the series list/detail no longer let one user enumerate another user's titles, covers, and statuses. The author page's embedded book list now applies the same owner predicate as the book list, so the two views agree on co-authored books. Existing unowned rows keep their legacy world-visible behaviour; only affects deployments with `BINDERY_ENFORCE_TENANCY` enabled. ### Fixed - **Import Mode UI no longer claims Move is the default** (#1444) — the selector pre-selected Move while the backend has defaulted to auto (hardlink on the same filesystem, else copy) since the seeding fix. Auto is now a first-class, selectable mode shown as the default, restoring a UI path back to the safe behaviour, and `import.mode` is validated so a typo fails loudly instead of silently behaving as auto. Contributed by @johnistheman. - **Per-account Hardcover reading lists** (#1489) — Hardcover's built-in shelves ("Want to Read", …) share one slug per account, so loading a second person's lists with their token showed their shelf as the already-added first one and toggled the wrong list. List identity is now (slug, account): the picker reports which Hardcover account it's browsing, saved lists remember the account they came from (shown as an @username chip), and two households' "Want to Read" lists sync side by side, each with its own token. - **Lists no longer silently truncate at 100 rows** (#1467) — the History page now pages through your full history on the server (with a working event-type filter that offers all event types), an author's page loads the complete catalogue even for authors with more than 100 books (so counts, filters, and select-all cover everything), and the Calendar loads every release in the month instead of stopping at 500. - **Transmission/qBittorrent: healthy downloads no longer blocked as "source no longer available"** (#1461) — the stale-failure check treated a category-filtered torrent listing as a complete one. A failed-import download whose torrent was moved to another download directory, lost its label, or sat under a different qBittorrent category (while the unfiltered listing was unavailable) was terminally blocked even though the torrent was still seeding. Filtered or degraded listings now only block on retry exhaustion, never on "missing from the list". - **Library scan single-flight** (#1460) — triggering a manual library scan while another scan is running (manual or the scheduled one) now returns 409 Conflict instead of starting a second concurrent walk that could race on book creation and clobber the last-scan status. - **Migration runner semicolon gotcha** (#1465) — the SQL migration runner split statements on `;` before stripping comments, so a semicolon inside a `--` comment (or a string literal) corrupted the statement list and aborted boot. The splitter is now aware of line comments, block comments, and quoted literals, so migration authors can write natural SQL comments. ## [v1.24.3] — 2026-07-10 A small security-driven patch. The Go toolchain moves to 1.26.5 to pick up two stdlib vulnerability fixes that are reachable from Bindery code paths, and the Grimmory connection test now says what it actually received when a server (or a reverse proxy in front of it) answers with something other than JSON. ### Security - **Go toolchain bumped to 1.26.5** (#1492) — rebuilds all binaries and images against a stdlib carrying the fixes for GO-2026-4970 (`crypto/tls`) and GO-2026-5856 (`os.Root`), both confirmed reachable in Bindery by `govulncheck`. v1.24.2 binaries were built with the affected stdlib; no Bindery code changes were needed. Workflow `go-version` pins and the main `Dockerfile` digest move together so CI and the published image agree on the toolchain. ### Fixed - **Grimmory connection errors now name the content type** (#1493) — adding a Grimmory connection whose URL answers 2xx with a non-JSON body (typically a reverse proxy or SPA fallback serving an HTML page on the API path) failed with the bare JSON decoder error `invalid character '<' looking for beginning of value`. The client now reports the HTTP status and `Content-Type` it got back and points at the URL as the likely culprit, so misrouted proxies are diagnosable from the Settings screen. Refs #1485. ## [v1.24.2] — 2026-07-07 A fast follow-up patch. The headline fix is Bulk Folder Import, which stalled for minutes and then failed with no logs on any library with a few hundred entries, because it reloaded the whole book and author catalogue once for every item in the folder. The rest is a batch of correctness and security fixes surfaced by an internal audit: two data-safety races on the download pipeline, a nightly refresh that wiped enriched author metadata, a Transmission queue mapping that failed healthy downloads, and two hardening fixes covering system logs and indexer keys. ### Security - **System logs are now admin-only** (#1451) — `/system/logs` and `/system/loglevel` were reachable by any authenticated user, exposing the app-wide log stream (other users' book and author names, OIDC usernames, download titles) and letting a non-admin flip the global log level. They now require admin, matching every other global-infra route. - **Indexer API keys no longer leak into errors** (#1450) — when a torrent/NZB fetch failed at the transport layer (timeout, DNS, TLS), the underlying `url.Error` carried the full signed download URL, including the indexer's `apikey`, into the download row, history, and webhook/Discord notifications. All five download-client fetch paths now scrub the key before the error is wrapped. ### Fixed - **Bulk Folder Import no longer times out on large libraries** (#1473) — scanning a folder used to reload the entire book and author catalogue once for every item, so a folder with a few hundred entries fired hundreds of full-table queries in a single request and ran past the server's request timeout, leaving the connection dead and no logs to explain it. The scan now loads the catalogue once and reuses it for every item, and it logs when a scan starts and finishes with how long it took and how many matches it found. - **Download status races** (#1462) — two things updating the same download at once could slip an illegal state change through (re-completing an already imported download or double stamping timestamps). The status update is now applied atomically so only one writer wins and the row can't land in a bad state. - **Transmission downloads queued behind others no longer fail to import** (#1452) — the RPC status enum was mapped wrong (`3` was treated as "seeding", `6` as "stopped"; they are actually "queued to download" and "seeding"). A torrent sitting in Transmission's download queue at 0% was treated as complete, import fired against an empty directory, failed, and after exhausting its retries the download was terminally blocked even though it was perfectly healthy. Completion is now keyed off real seeding / 100%-downloaded state. - **The wanted-book sweep no longer double-grabs** (#1453) — a Wanted book stays Wanted until its file is imported and reconciled, so a book whose grab was still downloading (or working through the import pipeline) was re-searched on the next scheduled sweep and, if the indexer now ranked a different release best, a second download was grabbed for the same book. The sweep now skips books with a grab already in flight, while still re-searching books whose previous download failed. - **Author metadata refresh no longer wipes enriched fields** (#1463) — the nightly refresh now keeps the existing description, ratings and rating count when the upstream record comes back sparse, instead of overwriting them with empty values. - **qBittorrent category rejections are no longer ignored** (#1464) — Bindery now notices when qBittorrent rejects a category assignment (for example a 409 because the category does not exist) instead of silently ignoring it, so the missing-category warning fires and a mis-categorized torrent no longer vanishes from the download poll. - **Grimmory "Test connection" now authenticates before probing status** (#1448) — recent Grimmory guards `/api/status` behind a valid session, so the test button returned a 401 for anyone using username/password auth. Bindery now logs in first and presents the token when checking connectivity (retrying once if a cached token has expired), so the test succeeds instead of failing at the door. - **Log search takes wildcards literally** (#1466) — searching the logs for a term containing `%`, `_`, or `\` now matches those characters as text instead of treating them as SQL wildcards, so you get the results you expect. - **Faster library, book, and search queries** (#1454) — the query that finds each book's ebook/audiobook file ran twice per book listing and was doing a full scan of the `book_files` table because no index covered its `format` filter. A new composite index makes it an index seek, most noticeable on large libraries and on the paginated Books page. Applied automatically on upgrade. - **Bulk "search" on the Wanted page is much faster** (#1455) — selecting a large batch and hitting search issued one of the heaviest book queries per book instead of a single batched fetch, so a 500-book bulk search ran hundreds of full-table aggregations. It now loads all selected books in one query. ## [v1.24.1] — 2026-07-06 A patch cut of fixes surfaced by the community in the days after v1.24.0: two were Bindery giving actively wrong information (a dead-end fix for NZBFinder's error 203, and Grimmory test failures rendering as an opaque "Bad Gateway"), and two were the library scan and bulk import silently mishandling flat `Author/Title.epub` layouts. ### Fixed - **Error 203 guidance no longer points at a setting that can't be changed** (#1424) — the NZB grab error and the Troubleshooting wiki suggested disabling Prowlarr's per-indexer Redirect setting, which Prowlarr requires for Usenet indexers. Both now explain the real situation: application-whitelisting indexers (like NZBFinder) have to approve Bindery's identity, tracked in #1425. - **Grimmory "Test Connection" failures now show the real error** (#1431) — a failed test displayed the bare HTTP status text ("Bad Gateway") instead of the actual diagnostic (connection refused, login rejected, upstream proxy error). The UI now surfaces the full message, upstream HTTP errors are labeled with their status code, and failures are logged server-side. - **Bulk folder import shows the full source path on every row** (#1435) — previously only the basename was shown, so an ambiguous match gave no way to tell which file the "pick a book" dropdown referred to. - **Library scan no longer hides untracked ebooks that share a folder with a tracked one** (#1436) — in a flat `Author/Title.epub` layout, one registered file made the scan silently skip every sibling ebook in that author folder, and the skipped files were missing from the result counts entirely. Folder-level suppression now only applies to audiobook folders, and the scan result shows an "Already tracked" count so files found always adds up. ## [v1.24.0] — 2026-07-04 The Grimmory integration goes from a settings tab nothing read to a working push pipeline, and the UI can now be embedded in a dashboard iframe when an operator explicitly opts in. A new per-author "Monitor newly discovered books" policy defuses the refresh-mass-monitors-the-back-catalogue trap. Three community bug reports filed this week are fixed in the same cut: books silently stranded under the wrong author, the author-page filters mishandling dual-format books, and unreadable NZB grab failures. Repo-side, the CI pipeline gains AI triage/review bots and a hardening pass on the fork-facing workflows. ### Added - **Grimmory push pipeline — BookDrop upload on import plus bulk sync** (#1392, closes #826) — the Grimmory integration was configuration-only: a `Ping()` client and a Settings toggle no code ever read. It now works end to end. A new `internal/grimmory` client does JWT auth against Grimmory's API (login → access/refresh pair, rotation, one 401 retry after re-auth; a set `api_key` is honoured as a static Bearer token and bypasses login), and streams a multipart upload to `POST /api/v1/files/upload/bookdrop` on a dedicated 5-minute timeout. A `Pusher` is hooked into the importer alongside the Calibre/CWA handoffs — settings are read live per push (no restart) and pushes are best-effort by contract, so a Grimmory failure never fails the import. BookDrop has no server-side dedup, so idempotency is Bindery's: migration 059 adds a `grimmory_pushes` table keyed by file path, consulted on every push. Bulk sync (`grimmory.Syncer`, admin-only `POST /grimmory/sync` + `GET /grimmory/sync/status`) mirrors the Calibre single-job pattern (409 on concurrent start, polled progress, capped error list). Settings → Grimmory gains username/password fields, a real Test Connection login check, a "Push all to Grimmory" button with live progress, and the experimental banner is gone. Ebook files only for now — BookDrop takes one file per upload, which multi-part audiobook folders don't reduce to. - **Opt-in iframe embedding via `BINDERY_FRAME_ANCESTORS`** (#1367) — the UI could not be embedded in an `